mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-14 15:37:55 +00:00
557 lines
19 KiB
JavaScript
557 lines
19 KiB
JavaScript
/*
|
|
#ifdef 0
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
#endif
|
|
*/
|
|
|
|
/**
|
|
* Controls the "full zoom" setting and its site-specific preferences.
|
|
*/
|
|
var FullZoom = {
|
|
// Identifies the setting in the content prefs database.
|
|
name: "browser.content.full-zoom",
|
|
|
|
// browser.zoom.siteSpecific preference cache
|
|
_siteSpecificPref: undefined,
|
|
|
|
// browser.zoom.updateBackgroundTabs preference cache
|
|
updateBackgroundTabs: undefined,
|
|
|
|
// One of the possible values for the mousewheel.* preferences.
|
|
// From nsEventStateManager.h.
|
|
ACTION_ZOOM: 3,
|
|
|
|
// This maps browser outer window IDs to monotonically increasing integer
|
|
// tokens. _browserTokenMap[outerID] is increased each time the zoom is
|
|
// changed in the browser whose outer window ID is outerID. See
|
|
// _getBrowserToken and _ignorePendingZoomAccesses.
|
|
_browserTokenMap: new Map(),
|
|
|
|
get siteSpecific() {
|
|
return this._siteSpecificPref;
|
|
},
|
|
|
|
//**************************************************************************//
|
|
// nsISupports
|
|
|
|
QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
|
|
Ci.nsIObserver,
|
|
Ci.nsIContentPrefObserver,
|
|
Ci.nsISupportsWeakReference,
|
|
Ci.nsISupports]),
|
|
|
|
//**************************************************************************//
|
|
// Initialization & Destruction
|
|
|
|
init: function FullZoom_init() {
|
|
// Bug 691614 - zooming support for electrolysis
|
|
if (gMultiProcessBrowser)
|
|
return;
|
|
|
|
// Listen for scrollwheel events so we can save scrollwheel-based changes.
|
|
window.addEventListener("DOMMouseScroll", this, false);
|
|
|
|
// Register ourselves with the service so we know when our pref changes.
|
|
this._cps2 = Cc["@mozilla.org/content-pref/service;1"].
|
|
getService(Ci.nsIContentPrefService2);
|
|
this._cps2.addObserverForName(this.name, this);
|
|
|
|
this._siteSpecificPref =
|
|
gPrefService.getBoolPref("browser.zoom.siteSpecific");
|
|
this.updateBackgroundTabs =
|
|
gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
|
|
// Listen for changes to the browser.zoom branch so we can enable/disable
|
|
// updating background tabs and per-site saving and restoring of zoom levels.
|
|
gPrefService.addObserver("browser.zoom.", this, true);
|
|
|
|
Services.obs.addObserver(this, "outer-window-destroyed", false);
|
|
},
|
|
|
|
destroy: function FullZoom_destroy() {
|
|
// Bug 691614 - zooming support for electrolysis
|
|
if (gMultiProcessBrowser)
|
|
return;
|
|
|
|
gPrefService.removeObserver("browser.zoom.", this);
|
|
this._cps2.removeObserverForName(this.name, this);
|
|
window.removeEventListener("DOMMouseScroll", this, false);
|
|
Services.obs.removeObserver(this, "outer-window-destroyed");
|
|
},
|
|
|
|
|
|
//**************************************************************************//
|
|
// Event Handlers
|
|
|
|
// nsIDOMEventListener
|
|
|
|
handleEvent: function FullZoom_handleEvent(event) {
|
|
switch (event.type) {
|
|
case "DOMMouseScroll":
|
|
this._handleMouseScrolled(event);
|
|
break;
|
|
}
|
|
},
|
|
|
|
_handleMouseScrolled: function FullZoom__handleMouseScrolled(event) {
|
|
// Construct the "mousewheel action" pref key corresponding to this event.
|
|
// Based on nsEventStateManager::WheelPrefs::GetBasePrefName().
|
|
var pref = "mousewheel.";
|
|
|
|
var pressedModifierCount = event.shiftKey + event.ctrlKey + event.altKey +
|
|
event.metaKey + event.getModifierState("OS");
|
|
if (pressedModifierCount != 1) {
|
|
pref += "default.";
|
|
} else if (event.shiftKey) {
|
|
pref += "with_shift.";
|
|
} else if (event.ctrlKey) {
|
|
pref += "with_control.";
|
|
} else if (event.altKey) {
|
|
pref += "with_alt.";
|
|
} else if (event.metaKey) {
|
|
pref += "with_meta.";
|
|
} else {
|
|
pref += "with_win.";
|
|
}
|
|
|
|
pref += "action";
|
|
|
|
// Don't do anything if this isn't a "zoom" scroll event.
|
|
var isZoomEvent = false;
|
|
try {
|
|
isZoomEvent = (gPrefService.getIntPref(pref) == this.ACTION_ZOOM);
|
|
} catch (e) {}
|
|
if (!isZoomEvent)
|
|
return;
|
|
|
|
// XXX Lazily cache all the possible action prefs so we don't have to get
|
|
// them anew from the pref service for every scroll event? We'd have to
|
|
// make sure to observe them so we can update the cache when they change.
|
|
|
|
// We have to call _applyZoomToPref in a timeout because we handle the
|
|
// event before the event state manager has a chance to apply the zoom
|
|
// during nsEventStateManager::PostHandleEvent.
|
|
let browser = gBrowser.selectedBrowser;
|
|
let token = this._getBrowserToken(browser);
|
|
window.setTimeout(function () {
|
|
if (token.isCurrent)
|
|
this._applyZoomToPref(browser);
|
|
}.bind(this), 0);
|
|
},
|
|
|
|
// nsIObserver
|
|
|
|
observe: function (aSubject, aTopic, aData) {
|
|
switch (aTopic) {
|
|
case "nsPref:changed":
|
|
switch (aData) {
|
|
case "browser.zoom.siteSpecific":
|
|
this._siteSpecificPref =
|
|
gPrefService.getBoolPref("browser.zoom.siteSpecific");
|
|
break;
|
|
case "browser.zoom.updateBackgroundTabs":
|
|
this.updateBackgroundTabs =
|
|
gPrefService.getBoolPref("browser.zoom.updateBackgroundTabs");
|
|
break;
|
|
}
|
|
break;
|
|
case "outer-window-destroyed":
|
|
let outerID = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
|
|
this._browserTokenMap.delete(outerID);
|
|
break;
|
|
}
|
|
},
|
|
|
|
// nsIContentPrefObserver
|
|
|
|
onContentPrefSet: function FullZoom_onContentPrefSet(aGroup, aName, aValue) {
|
|
this._onContentPrefChanged(aGroup, aValue);
|
|
},
|
|
|
|
onContentPrefRemoved: function FullZoom_onContentPrefRemoved(aGroup, aName) {
|
|
this._onContentPrefChanged(aGroup, undefined);
|
|
},
|
|
|
|
/**
|
|
* Appropriately updates the zoom level after a content preference has
|
|
* changed.
|
|
*
|
|
* @param aGroup The group of the changed preference.
|
|
* @param aValue The new value of the changed preference. Pass undefined to
|
|
* indicate the preference's removal.
|
|
*/
|
|
_onContentPrefChanged: function FullZoom__onContentPrefChanged(aGroup, aValue) {
|
|
if (this._isNextContentPrefChangeInternal) {
|
|
// Ignore changes that FullZoom itself makes. This works because the
|
|
// content pref service calls callbacks before notifying observers, and it
|
|
// does both in the same turn of the event loop.
|
|
delete this._isNextContentPrefChangeInternal;
|
|
return;
|
|
}
|
|
|
|
let browser = gBrowser.selectedBrowser;
|
|
if (!browser.currentURI)
|
|
return;
|
|
|
|
let domain = this._cps2.extractDomain(browser.currentURI.spec);
|
|
if (aGroup) {
|
|
if (aGroup == domain)
|
|
this._applyPrefToZoom(aValue, browser);
|
|
return;
|
|
}
|
|
|
|
this._globalValue = aValue === undefined ? aValue :
|
|
this._ensureValid(aValue);
|
|
|
|
// If the current page doesn't have a site-specific preference, then its
|
|
// zoom should be set to the new global preference now that the global
|
|
// preference has changed.
|
|
let hasPref = false;
|
|
let ctxt = this._loadContextFromWindow(browser.contentWindow);
|
|
let token = this._getBrowserToken(browser);
|
|
this._cps2.getByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
|
|
handleResult: function () hasPref = true,
|
|
handleCompletion: function () {
|
|
if (!hasPref && token.isCurrent)
|
|
this._applyPrefToZoom(undefined, browser);
|
|
}.bind(this)
|
|
});
|
|
},
|
|
|
|
// location change observer
|
|
|
|
/**
|
|
* Called when the location of a tab changes.
|
|
* When that happens, we need to update the current zoom level if appropriate.
|
|
*
|
|
* @param aURI
|
|
* A URI object representing the new location.
|
|
* @param aIsTabSwitch
|
|
* Whether this location change has happened because of a tab switch.
|
|
* @param aBrowser
|
|
* (optional) browser object displaying the document
|
|
*/
|
|
onLocationChange: function FullZoom_onLocationChange(aURI, aIsTabSwitch, aBrowser) {
|
|
// Ignore all pending async zoom accesses in the browser. Pending accesses
|
|
// that started before the location change will be prevented from applying
|
|
// to the new location.
|
|
let browser = aBrowser || gBrowser.selectedBrowser;
|
|
this._ignorePendingZoomAccesses(browser);
|
|
|
|
// Bug 691614 - zooming support for electrolysis
|
|
if (gMultiProcessBrowser)
|
|
return;
|
|
|
|
if (!aURI || (aIsTabSwitch && !this.siteSpecific)) {
|
|
this._notifyOnLocationChange();
|
|
return;
|
|
}
|
|
|
|
// Avoid the cps roundtrip and apply the default/global pref.
|
|
if (aURI.spec == "about:blank") {
|
|
this._applyPrefToZoom(undefined, browser,
|
|
this._notifyOnLocationChange.bind(this));
|
|
return;
|
|
}
|
|
|
|
// Media documents should always start at 1, and are not affected by prefs.
|
|
if (!aIsTabSwitch && browser.contentDocument.mozSyntheticDocument) {
|
|
ZoomManager.setZoomForBrowser(browser, 1);
|
|
// _ignorePendingZoomAccesses already called above, so no need here.
|
|
this._notifyOnLocationChange();
|
|
return;
|
|
}
|
|
|
|
// See if the zoom pref is cached.
|
|
let ctxt = this._loadContextFromWindow(browser.contentWindow);
|
|
let pref = this._cps2.getCachedByDomainAndName(aURI.spec, this.name, ctxt);
|
|
if (pref) {
|
|
this._applyPrefToZoom(pref.value, browser,
|
|
this._notifyOnLocationChange.bind(this));
|
|
return;
|
|
}
|
|
|
|
// It's not cached, so we have to asynchronously fetch it.
|
|
let value = undefined;
|
|
let token = this._getBrowserToken(browser);
|
|
this._cps2.getByDomainAndName(aURI.spec, this.name, ctxt, {
|
|
handleResult: function (resultPref) value = resultPref.value,
|
|
handleCompletion: function () {
|
|
if (!token.isCurrent) {
|
|
this._notifyOnLocationChange();
|
|
return;
|
|
}
|
|
this._applyPrefToZoom(value, browser,
|
|
this._notifyOnLocationChange.bind(this));
|
|
}.bind(this)
|
|
});
|
|
},
|
|
|
|
// update state of zoom type menu item
|
|
|
|
updateMenu: function FullZoom_updateMenu() {
|
|
var menuItem = document.getElementById("toggle_zoom");
|
|
|
|
menuItem.setAttribute("checked", !ZoomManager.useFullZoom);
|
|
},
|
|
|
|
//**************************************************************************//
|
|
// Setting & Pref Manipulation
|
|
|
|
/**
|
|
* Reduces the zoom level of the page in the current browser.
|
|
*/
|
|
reduce: function FullZoom_reduce() {
|
|
ZoomManager.reduce();
|
|
let browser = gBrowser.selectedBrowser;
|
|
this._ignorePendingZoomAccesses(browser);
|
|
this._applyZoomToPref(browser);
|
|
},
|
|
|
|
/**
|
|
* Enlarges the zoom level of the page in the current browser.
|
|
*/
|
|
enlarge: function FullZoom_enlarge() {
|
|
ZoomManager.enlarge();
|
|
let browser = gBrowser.selectedBrowser;
|
|
this._ignorePendingZoomAccesses(browser);
|
|
this._applyZoomToPref(browser);
|
|
},
|
|
|
|
/**
|
|
* Sets the zoom level of the page in the current browser to the global zoom
|
|
* level.
|
|
*/
|
|
reset: function FullZoom_reset() {
|
|
let browser = gBrowser.selectedBrowser;
|
|
let token = this._getBrowserToken(browser);
|
|
this._getGlobalValue(browser.contentWindow, function (value) {
|
|
if (token.isCurrent) {
|
|
ZoomManager.setZoomForBrowser(browser, value === undefined ? 1 : value);
|
|
this._ignorePendingZoomAccesses(browser);
|
|
}
|
|
});
|
|
this._removePref(browser);
|
|
},
|
|
|
|
/**
|
|
* Set the zoom level for a given browser.
|
|
*
|
|
* Per nsPresContext::setFullZoom, we can set the zoom to its current value
|
|
* without significant impact on performance, as the setting is only applied
|
|
* if it differs from the current setting. In fact getting the zoom and then
|
|
* checking ourselves if it differs costs more.
|
|
*
|
|
* And perhaps we should always set the zoom even if it was more expensive,
|
|
* since nsDocumentViewer::SetTextZoom claims that child documents can have
|
|
* a different text zoom (although it would be unusual), and it implies that
|
|
* those child text zooms should get updated when the parent zoom gets set,
|
|
* and perhaps the same is true for full zoom
|
|
* (although nsDocumentViewer::SetFullZoom doesn't mention it).
|
|
*
|
|
* So when we apply new zoom values to the browser, we simply set the zoom.
|
|
* We don't check first to see if the new value is the same as the current
|
|
* one.
|
|
*
|
|
* @param aValue The zoom level value.
|
|
* @param aBrowser The zoom is set in this browser. Required.
|
|
* @param aCallback If given, it's asynchronously called when complete.
|
|
*/
|
|
_applyPrefToZoom: function FullZoom__applyPrefToZoom(aValue, aBrowser, aCallback) {
|
|
if (!this.siteSpecific || gInPrintPreviewMode) {
|
|
this._executeSoon(aCallback);
|
|
return;
|
|
}
|
|
|
|
// aBrowser.contentDocument is sometimes gone because this method is called
|
|
// by content pref service callbacks, which themselves can be called at any
|
|
// time, even after browsers are closed.
|
|
if (!aBrowser.contentDocument ||
|
|
aBrowser.contentDocument.mozSyntheticDocument) {
|
|
this._executeSoon(aCallback);
|
|
return;
|
|
}
|
|
|
|
if (aValue !== undefined) {
|
|
ZoomManager.setZoomForBrowser(aBrowser, this._ensureValid(aValue));
|
|
this._ignorePendingZoomAccesses(aBrowser);
|
|
this._executeSoon(aCallback);
|
|
return;
|
|
}
|
|
|
|
let token = this._getBrowserToken(aBrowser);
|
|
this._getGlobalValue(aBrowser.contentWindow, function (value) {
|
|
if (token.isCurrent) {
|
|
ZoomManager.setZoomForBrowser(aBrowser, value === undefined ? 1 : value);
|
|
this._ignorePendingZoomAccesses(aBrowser);
|
|
}
|
|
this._executeSoon(aCallback);
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Saves the zoom level of the page in the given browser to the content
|
|
* prefs store.
|
|
*
|
|
* @param browser The zoom of this browser will be saved. Required.
|
|
*/
|
|
_applyZoomToPref: function FullZoom__applyZoomToPref(browser) {
|
|
if (!this.siteSpecific ||
|
|
gInPrintPreviewMode ||
|
|
browser.contentDocument.mozSyntheticDocument)
|
|
return;
|
|
|
|
this._cps2.set(browser.currentURI.spec, this.name,
|
|
ZoomManager.getZoomForBrowser(browser),
|
|
this._loadContextFromWindow(browser.contentWindow), {
|
|
handleCompletion: function () {
|
|
this._isNextContentPrefChangeInternal = true;
|
|
}.bind(this),
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Removes from the content prefs store the zoom level of the given browser.
|
|
*
|
|
* @param browser The zoom of this browser will be removed. Required.
|
|
*/
|
|
_removePref: function FullZoom__removePref(browser) {
|
|
if (browser.contentDocument.mozSyntheticDocument)
|
|
return;
|
|
let ctxt = this._loadContextFromWindow(browser.contentWindow);
|
|
this._cps2.removeByDomainAndName(browser.currentURI.spec, this.name, ctxt, {
|
|
handleCompletion: function () {
|
|
this._isNextContentPrefChangeInternal = true;
|
|
}.bind(this),
|
|
});
|
|
},
|
|
|
|
//**************************************************************************//
|
|
// Utilities
|
|
|
|
/**
|
|
* Returns the zoom change token of the given browser. Asynchronous
|
|
* operations that access the given browser's zoom should use this method to
|
|
* capture the token before starting and use token.isCurrent to determine if
|
|
* it's safe to access the zoom when done. If token.isCurrent is false, then
|
|
* the zoom of the browser was changed after the async operation started, and
|
|
* depending on what the operation is doing, it may no longer be safe to set
|
|
* the zoom or get it to then use in some manner.
|
|
*
|
|
* @param browser The token of this browser will be returned.
|
|
* @return An object with an "isCurrent" getter.
|
|
*/
|
|
_getBrowserToken: function FullZoom__getBrowserToken(browser) {
|
|
let outerID = this._browserOuterID(browser);
|
|
let map = this._browserTokenMap;
|
|
if (!map.has(outerID))
|
|
map.set(outerID, 0);
|
|
return {
|
|
token: map.get(outerID),
|
|
get isCurrent() {
|
|
return map.get(outerID) === this.token;
|
|
},
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Increments the zoom change token for the given browser so that pending
|
|
* async operations know that it may be unsafe to access they zoom when they
|
|
* finish.
|
|
*
|
|
* @param browser Pending accesses in this browser will be ignored.
|
|
*/
|
|
_ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) {
|
|
let outerID = this._browserOuterID(browser);
|
|
let map = this._browserTokenMap;
|
|
map.set(outerID, (map.get(outerID) || 0) + 1);
|
|
},
|
|
|
|
_browserOuterID: function FullZoom__browserOuterID(browser) {
|
|
return browser.
|
|
contentWindow.
|
|
QueryInterface(Ci.nsIInterfaceRequestor).
|
|
getInterface(Ci.nsIDOMWindowUtils).
|
|
outerWindowID;
|
|
},
|
|
|
|
_ensureValid: function FullZoom__ensureValid(aValue) {
|
|
// Note that undefined is a valid value for aValue that indicates a known-
|
|
// not-to-exist value.
|
|
if (isNaN(aValue))
|
|
return 1;
|
|
|
|
if (aValue < ZoomManager.MIN)
|
|
return ZoomManager.MIN;
|
|
|
|
if (aValue > ZoomManager.MAX)
|
|
return ZoomManager.MAX;
|
|
|
|
return aValue;
|
|
},
|
|
|
|
/**
|
|
* Gets the global browser.content.full-zoom content preference.
|
|
*
|
|
* WARNING: callback may be called synchronously or asynchronously. The
|
|
* reason is that it's usually desirable to avoid turns of the event loop
|
|
* where possible, since they can lead to visible, jarring jumps in zoom
|
|
* level. It's not always possible to avoid them, though. As a convenience,
|
|
* then, this method takes a callback and returns nothing.
|
|
*
|
|
* @param window The content window pertaining to the zoom.
|
|
* @param callback Synchronously or asynchronously called when done. It's
|
|
* bound to this object (FullZoom) and called as:
|
|
* callback(prefValue)
|
|
*/
|
|
_getGlobalValue: function FullZoom__getGlobalValue(window, callback) {
|
|
// * !("_globalValue" in this) => global value not yet cached.
|
|
// * this._globalValue === undefined => global value known not to exist.
|
|
// * Otherwise, this._globalValue is a number, the global value.
|
|
if ("_globalValue" in this) {
|
|
callback.call(this, this._globalValue, true);
|
|
return;
|
|
}
|
|
let value = undefined;
|
|
this._cps2.getGlobal(this.name, this._loadContextFromWindow(window), {
|
|
handleResult: function (pref) value = pref.value,
|
|
handleCompletion: function (reason) {
|
|
this._globalValue = this._ensureValid(value);
|
|
callback.call(this, this._globalValue);
|
|
}.bind(this)
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Gets the load context from the given window.
|
|
*
|
|
* @param window The window whose load context will be returned.
|
|
* @return The nsILoadContext of the given window.
|
|
*/
|
|
_loadContextFromWindow: function FullZoom__loadContextFromWindow(window) {
|
|
return window.
|
|
QueryInterface(Ci.nsIInterfaceRequestor).
|
|
getInterface(Ci.nsIWebNavigation).
|
|
QueryInterface(Ci.nsILoadContext);
|
|
},
|
|
|
|
/**
|
|
* Asynchronously broadcasts a "browser-fullZoom:locationChange" notification
|
|
* so that tests can select tabs, load pages, etc. and be notified when the
|
|
* zoom levels on those pages change. The notification is always asynchronous
|
|
* so that observers are guaranteed a consistent behavior.
|
|
*/
|
|
_notifyOnLocationChange: function FullZoom__notifyOnLocationChange() {
|
|
this._executeSoon(function () {
|
|
Services.obs.notifyObservers(null, "browser-fullZoom:locationChange", "");
|
|
});
|
|
},
|
|
|
|
_executeSoon: function FullZoom__executeSoon(callback) {
|
|
if (!callback)
|
|
return;
|
|
Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
|
|
},
|
|
};
|