|
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 // One of the possible values for the mousewheel.* preferences. |
|
23 // From EventStateManager.h. |
|
24 ACTION_ZOOM: 3, |
|
25 |
|
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(), |
|
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 // Listen for scrollwheel events so we can save scrollwheel-based changes. |
|
49 window.addEventListener("DOMMouseScroll", this, false); |
|
50 |
|
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); |
|
55 |
|
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 }, |
|
64 |
|
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 }, |
|
70 |
|
71 |
|
72 //**************************************************************************// |
|
73 // Event Handlers |
|
74 |
|
75 // nsIDOMEventListener |
|
76 |
|
77 handleEvent: function FullZoom_handleEvent(event) { |
|
78 switch (event.type) { |
|
79 case "DOMMouseScroll": |
|
80 this._handleMouseScrolled(event); |
|
81 break; |
|
82 } |
|
83 }, |
|
84 |
|
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."; |
|
89 |
|
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 } |
|
105 |
|
106 pref += "action"; |
|
107 |
|
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; |
|
115 |
|
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. |
|
119 |
|
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 }, |
|
130 |
|
131 // nsIObserver |
|
132 |
|
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 }, |
|
149 |
|
150 // nsIContentPrefObserver |
|
151 |
|
152 onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue) { |
|
153 this._onContentPrefChanged(aGroup, aValue); |
|
154 }, |
|
155 |
|
156 onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName) { |
|
157 this._onContentPrefChanged(aGroup, undefined); |
|
158 }, |
|
159 |
|
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 } |
|
176 |
|
177 let browser = gBrowser.selectedBrowser; |
|
178 if (!browser.currentURI) |
|
179 return; |
|
180 |
|
181 let domain = this._cps2.extractDomain(browser.currentURI.spec); |
|
182 if (aGroup) { |
|
183 if (aGroup == domain) |
|
184 this._applyPrefToZoom(aValue, browser); |
|
185 return; |
|
186 } |
|
187 |
|
188 this._globalValue = aValue === undefined ? aValue : |
|
189 this._ensureValid(aValue); |
|
190 |
|
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 }, |
|
205 |
|
206 // location change observer |
|
207 |
|
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); |
|
225 |
|
226 if (!aURI || (aIsTabSwitch && !this.siteSpecific)) { |
|
227 this._notifyOnLocationChange(); |
|
228 return; |
|
229 } |
|
230 |
|
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 } |
|
237 |
|
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 } |
|
245 |
|
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 } |
|
254 |
|
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 }, |
|
270 |
|
271 // update state of zoom type menu item |
|
272 |
|
273 updateMenu: function FullZoom_updateMenu() { |
|
274 var menuItem = document.getElementById("toggle_zoom"); |
|
275 |
|
276 menuItem.setAttribute("checked", !ZoomManager.useFullZoom); |
|
277 }, |
|
278 |
|
279 //**************************************************************************// |
|
280 // Setting & Pref Manipulation |
|
281 |
|
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 }, |
|
291 |
|
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 }, |
|
301 |
|
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 }, |
|
322 |
|
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 } |
|
351 |
|
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 } |
|
359 |
|
360 if (aValue !== undefined) { |
|
361 ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue)); |
|
362 this._ignorePendingZoomAccesses(aBrowser); |
|
363 this._executeSoon(aCallback); |
|
364 return; |
|
365 } |
|
366 |
|
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 }, |
|
376 |
|
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; |
|
389 |
|
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 }, |
|
398 |
|
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 }, |
|
415 |
|
416 //**************************************************************************// |
|
417 // Utilities |
|
418 |
|
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 }, |
|
447 |
|
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 }, |
|
459 |
|
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; |
|
465 |
|
466 if (aValue < ZoomManager.MIN) |
|
467 return ZoomManager.MIN; |
|
468 |
|
469 if (aValue > ZoomManager.MAX) |
|
470 return ZoomManager.MAX; |
|
471 |
|
472 return aValue; |
|
473 }, |
|
474 |
|
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 }, |
|
506 |
|
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 }, |
|
516 |
|
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 }, |
|
528 |
|
529 _executeSoon: function FullZoom__executeSoon(callback) { |
|
530 if (!callback) |
|
531 return; |
|
532 Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL); |
|
533 }, |
|
534 }; |