Add Pale Moon

This commit is contained in:
NTD 2018-02-02 03:35:06 -05:00 committed by Roy Tam
commit fe8028fa2e
1173 changed files with 143053 additions and 934 deletions

View file

@ -0,0 +1,67 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = [ "AboutHomeUtils" ];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
this.AboutHomeUtils = {
/**
* Returns an object containing the name and searchURL of the original default
* search engine.
*/
get defaultSearchEngine() {
let defaultEngine = Services.search.defaultEngine;
let submission = defaultEngine.getSubmission("_searchTerms_", null, "homepage");
return Object.freeze({
name: defaultEngine.name,
searchURL: submission.uri.spec,
postDataString: submission.postDataString
});
},
/*
* showKnowYourRights - Determines if the user should be shown the
* about:rights notification. The notification should *not* be shown if
* we've already shown the current version, or if the override pref says to
* never show it. The notification *should* be shown if it's never been seen
* before, if a newer version is available, or if the override pref says to
* always show it.
*/
get showKnowYourRights() {
// Look for an unconditional override pref. If set, do what it says.
// (true --> never show, false --> always show)
try {
return !Services.prefs.getBoolPref("browser.rights.override");
} catch (e) { }
// Ditto, for the legacy EULA pref.
try {
return !Services.prefs.getBoolPref("browser.EULA.override");
} catch (e) { }
#ifndef MOZILLA_OFFICIAL
// Non-official builds shouldn't show the notification.
return false;
#endif
// Look to see if the user has seen the current version or not.
var currentVersion = Services.prefs.getIntPref("browser.rights.version");
try {
return !Services.prefs.getBoolPref("browser.rights." + currentVersion + ".shown");
} catch (e) { }
// Legacy: If the user accepted a EULA, we won't annoy them with the
// equivalent about:rights page until the version changes.
try {
return !Services.prefs.getBoolPref("browser.EULA." + currentVersion + ".accepted");
} catch (e) { }
// We haven't shown the notification before, so do so now.
return true;
}
};

View file

@ -0,0 +1,436 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["BrowserNewTabPreloader"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const XUL_PAGE = "data:application/vnd.mozilla.xul+xml;charset=utf-8,<window%20id='win'/>";
const NEWTAB_URL = "about:newtab";
const PREF_BRANCH = "browser.newtab.";
// The interval between swapping in a preload docShell and kicking off the
// next preload in the background.
const PRELOADER_INTERVAL_MS = 600;
// The initial delay before we start preloading our first new tab page. The
// timer is started after the first 'browser-delayed-startup' has been sent.
const PRELOADER_INIT_DELAY_MS = 5000;
// The number of miliseconds we'll wait after we received a notification that
// causes us to update our list of browsers and tabbrowser sizes. This acts as
// kind of a damper when too many events are occuring in quick succession.
const PRELOADER_UPDATE_DELAY_MS = 3000;
const TOPIC_TIMER_CALLBACK = "timer-callback";
const TOPIC_DELAYED_STARTUP = "browser-delayed-startup-finished";
const TOPIC_XUL_WINDOW_CLOSED = "xul-window-destroyed";
function createTimer(obj, delay) {
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.init(obj, delay, Ci.nsITimer.TYPE_ONE_SHOT);
return timer;
}
function clearTimer(timer) {
if (timer) {
timer.cancel();
}
return null;
}
this.BrowserNewTabPreloader = {
init: function Preloader_init() {
Initializer.start();
},
uninit: function Preloader_uninit() {
Initializer.stop();
HostFrame.destroy();
Preferences.uninit();
HiddenBrowsers.uninit();
},
newTab: function Preloader_newTab(aTab) {
let win = aTab.ownerDocument.defaultView;
if (win.gBrowser) {
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
let {width, height} = utils.getBoundsWithoutFlushing(win.gBrowser);
let hiddenBrowser = HiddenBrowsers.get(width, height)
if (hiddenBrowser) {
return hiddenBrowser.swapWithNewTab(aTab);
}
}
return false;
}
};
Object.freeze(BrowserNewTabPreloader);
let Initializer = {
_timer: null,
_observing: false,
start: function Initializer_start() {
Services.obs.addObserver(this, TOPIC_DELAYED_STARTUP, false);
this._observing = true;
},
stop: function Initializer_stop() {
this._timer = clearTimer(this._timer);
if (this._observing) {
Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
this._observing = false;
}
},
observe: function Initializer_observe(aSubject, aTopic, aData) {
if (aTopic == TOPIC_DELAYED_STARTUP) {
Services.obs.removeObserver(this, TOPIC_DELAYED_STARTUP);
this._observing = false;
this._startTimer();
} else if (aTopic == TOPIC_TIMER_CALLBACK) {
this._timer = null;
this._startPreloader();
}
},
_startTimer: function Initializer_startTimer() {
this._timer = createTimer(this, PRELOADER_INIT_DELAY_MS);
},
_startPreloader: function Initializer_startPreloader() {
Preferences.init();
if (Preferences.enabled) {
HiddenBrowsers.init();
}
}
};
let Preferences = {
_enabled: null,
_branch: null,
get enabled() {
if (this._enabled === null) {
this._enabled = this._branch.getBoolPref("preload") &&
!this._branch.prefHasUserValue("url");
}
return this._enabled;
},
init: function Preferences_init() {
this._branch = Services.prefs.getBranch(PREF_BRANCH);
this._branch.addObserver("", this, false);
},
uninit: function Preferences_uninit() {
if (this._branch) {
this._branch.removeObserver("", this);
this._branch = null;
}
},
observe: function Preferences_observe() {
let prevEnabled = this._enabled;
this._enabled = null;
if (prevEnabled && !this.enabled) {
HiddenBrowsers.uninit();
} else if (!prevEnabled && this.enabled) {
HiddenBrowsers.init();
}
},
};
let HiddenBrowsers = {
_browsers: null,
_updateTimer: null,
_topics: [
TOPIC_DELAYED_STARTUP,
TOPIC_XUL_WINDOW_CLOSED
],
init: function () {
this._browsers = new Map();
this._updateBrowserSizes();
this._topics.forEach(t => Services.obs.addObserver(this, t, false));
},
uninit: function () {
if (this._browsers) {
this._topics.forEach(t => Services.obs.removeObserver(this, t, false));
this._updateTimer = clearTimer(this._updateTimer);
for (let [key, browser] of this._browsers) {
browser.destroy();
}
this._browsers = null;
}
},
get: function (width, height) {
// We haven't been initialized, yet.
if (!this._browsers) {
return null;
}
let key = width + "x" + height;
if (!this._browsers.has(key)) {
// Update all browsers' sizes if we can't find a matching one.
this._updateBrowserSizes();
}
// We should now have a matching browser.
if (this._browsers.has(key)) {
return this._browsers.get(key);
}
// We should never be here. Return the first browser we find.
Cu.reportError("NewTabPreloader: no matching browser found after updating");
for (let [size, browser] of this._browsers) {
return browser;
}
// We should really never be here.
Cu.reportError("NewTabPreloader: not even a single browser was found?");
return null;
},
observe: function (subject, topic, data) {
if (topic === TOPIC_TIMER_CALLBACK) {
this._updateTimer = null;
this._updateBrowserSizes();
} else {
this._updateTimer = clearTimer(this._updateTimer);
this._updateTimer = createTimer(this, PRELOADER_UPDATE_DELAY_MS);
}
},
_updateBrowserSizes: function () {
let sizes = this._collectTabBrowserSizes();
let toRemove = [];
// Iterate all browsers and check that they
// each can be assigned to one of the sizes.
for (let [key, browser] of this._browsers) {
if (sizes.has(key)) {
// We already have a browser for that size, great!
sizes.delete(key);
} else {
// This browser is superfluous or needs to be resized.
toRemove.push(browser);
this._browsers.delete(key);
}
}
// Iterate all sizes that we couldn't find a browser for.
for (let [key, {width, height}] of sizes) {
let browser;
if (toRemove.length) {
// Let's just resize one of the superfluous
// browsers and put it back into the map.
browser = toRemove.shift();
browser.resize(width, height);
} else {
// No more browsers to reuse, create a new one.
browser = new HiddenBrowser(width, height);
}
this._browsers.set(key, browser);
}
// Finally, remove all browsers we don't need anymore.
toRemove.forEach(b => b.destroy());
},
_collectTabBrowserSizes: function () {
let sizes = new Map();
function tabBrowserBounds() {
let wins = Services.ww.getWindowEnumerator("navigator:browser");
while (wins.hasMoreElements()) {
let win = wins.getNext();
if (win.gBrowser) {
let utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
yield utils.getBoundsWithoutFlushing(win.gBrowser);
}
}
}
// Collect the sizes of all <tabbrowser>s out there.
for (let {width, height} of tabBrowserBounds()) {
if (width > 0 && height > 0) {
let key = width + "x" + height;
if (!sizes.has(key)) {
sizes.set(key, {width: width, height: height});
}
}
}
return sizes;
}
};
function HiddenBrowser(width, height) {
this.resize(width, height);
HostFrame.get().then(aFrame => {
let doc = aFrame.document;
this._browser = doc.createElementNS(XUL_NS, "browser");
this._browser.setAttribute("type", "content");
this._browser.setAttribute("src", NEWTAB_URL);
this._applySize();
doc.getElementById("win").appendChild(this._browser);
});
}
HiddenBrowser.prototype = {
_width: null,
_height: null,
_timer: null,
_needsFrameScripts: true,
get isPreloaded() {
return this._browser &&
this._browser.contentDocument &&
this._browser.contentDocument.readyState === "complete" &&
this._browser.currentURI.spec === NEWTAB_URL;
},
swapWithNewTab: function (aTab) {
if (!this.isPreloaded || this._timer) {
return false;
}
let win = aTab.ownerDocument.defaultView;
let tabbrowser = win.gBrowser;
if (!tabbrowser) {
return false;
}
// Swap docShells.
tabbrowser.swapNewTabWithBrowser(aTab, this._browser);
// Load all default frame scripts.
if (this._needsFrameScripts) {
this._needsFrameScripts = false;
let mm = aTab.linkedBrowser.messageManager;
mm.loadFrameScript("chrome://browser/content/content.js", true);
mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
if ("TabView" in win) {
mm.loadFrameScript("chrome://browser/content/tabview-content.js", true);
}
}
// Start a timer that will kick off preloading the next newtab page.
this._timer = createTimer(this, PRELOADER_INTERVAL_MS);
// Signal that we swapped docShells.
return true;
},
observe: function () {
this._timer = null;
// Start pre-loading the new tab page.
this._browser.loadURI(NEWTAB_URL);
},
resize: function (width, height) {
this._width = width;
this._height = height;
this._applySize();
},
_applySize: function () {
if (this._browser) {
this._browser.style.width = this._width + "px";
this._browser.style.height = this._height + "px";
}
},
destroy: function () {
if (this._browser) {
this._browser.remove();
this._browser = null;
}
this._timer = clearTimer(this._timer);
}
};
let HostFrame = {
_frame: null,
_deferred: null,
get hiddenDOMDocument() {
return Services.appShell.hiddenDOMWindow.document;
},
get isReady() {
return this.hiddenDOMDocument.readyState === "complete";
},
get: function () {
if (!this._deferred) {
this._deferred = Promise.defer();
this._create();
}
return this._deferred.promise;
},
destroy: function () {
if (this._frame) {
if (!Cu.isDeadWrapper(this._frame)) {
this._frame.removeEventListener("load", this, true);
this._frame.remove();
}
this._frame = null;
this._deferred = null;
}
},
handleEvent: function () {
let contentWindow = this._frame.contentWindow;
if (contentWindow.location.href === XUL_PAGE) {
this._frame.removeEventListener("load", this, true);
this._deferred.resolve(contentWindow);
} else {
contentWindow.location = XUL_PAGE;
}
},
_create: function () {
if (this.isReady) {
let doc = this.hiddenDOMDocument;
this._frame = doc.createElementNS(HTML_NS, "iframe");
this._frame.addEventListener("load", this, true);
doc.documentElement.appendChild(this._frame);
} else {
let flags = Ci.nsIThread.DISPATCH_NORMAL;
Services.tm.currentThread.dispatch(() => this._create(), flags);
}
}
};

View file

@ -0,0 +1,160 @@
/* 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/. */
this.EXPORTED_SYMBOLS = [ "CharsetMenu" ];
const { classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gBundle", function() {
const kUrl = "chrome://browser/locale/charsetMenu.properties";
return Services.strings.createBundle(kUrl);
});
/**
* This set contains encodings that are in the Encoding Standard, except:
* - XSS-dangerous encodings (except ISO-2022-JP which is assumed to be
* too common not to be included).
* - x-user-defined, which practically never makes sense as an end-user-chosen
* override.
* - Encodings that IE11 doesn't have in its correspoding menu.
*/
const kEncodings = new Set([
// Globally relevant
"UTF-8",
"windows-1252",
// Arabic
"windows-1256",
"ISO-8859-6",
// Baltic
"windows-1257",
"ISO-8859-4",
// "ISO-8859-13", // Hidden since not in menu in IE11
// Central European
"windows-1250",
"ISO-8859-2",
// Chinese, Simplified
"gbk",
"gb18030",
// Chinese, Traditional
"Big5",
// Cyrillic
"windows-1251",
"ISO-8859-5",
"KOI8-R",
"KOI8-U",
"IBM866", // Not in menu in Chromium. Maybe drop this?
// "x-mac-cyrillic", // Not in menu in IE11 or Chromium.
// Greek
"windows-1253",
"ISO-8859-7",
// Hebrew
"windows-1255",
"ISO-8859-8-I",
"ISO-8859-8",
// Japanese
"Shift_JIS",
"EUC-JP",
"ISO-2022-JP",
// Korean
"EUC-KR",
// Thai
"windows-874",
// Turkish
"windows-1254",
// Vietnamese
"windows-1258",
// Hiding rare European encodings that aren't in the menu in IE11 and would
// make the menu messy by sorting all over the place
// "ISO-8859-3",
// "ISO-8859-10",
// "ISO-8859-14",
// "ISO-8859-15",
// "ISO-8859-16",
// "macintosh"
]);
// Always at the start of the menu, in this order, followed by a separator.
const kPinned = [
"UTF-8",
"windows-1252"
];
this.CharsetMenu = Object.freeze({
build: function BuildCharsetMenu(event) {
let parent = event.target;
if (parent.lastChild.localName != "menuseparator") {
// Detector menu or charset menu already built
return;
}
let doc = parent.ownerDocument;
function createItem(encoding) {
let menuItem = doc.createElement("menuitem");
menuItem.setAttribute("type", "radio");
menuItem.setAttribute("name", "charsetGroup");
try {
menuItem.setAttribute("label", gBundle.GetStringFromName(encoding));
} catch (e) {
// Localization error but put *something* in the menu to recover.
menuItem.setAttribute("label", encoding);
}
try {
menuItem.setAttribute("accesskey",
gBundle.GetStringFromName(encoding + ".key"));
} catch (e) {
// Some items intentionally don't have an accesskey
}
menuItem.setAttribute("id", "charset." + encoding);
return menuItem;
}
// Clone the set in order to be able to remove the pinned encodings from
// the cloned set.
let encodings = new Set(kEncodings);
for (let encoding of kPinned) {
encodings.delete(encoding);
parent.appendChild(createItem(encoding));
}
parent.appendChild(doc.createElement("menuseparator"));
let list = [];
for (let encoding of encodings) {
list.push(createItem(encoding));
}
list.sort(function (a, b) {
let titleA = a.getAttribute("label");
let titleB = b.getAttribute("label");
// Normal sorting sorts the part in parenthesis in an order that
// happens to make the less frequently-used items first.
let index;
if ((index = titleA.indexOf("(")) > -1) {
titleA = titleA.substring(0, index);
}
if ((index = titleB.indexOf("(")) > -1) {
titleA = titleB.substring(0, index);
}
let comp = titleA.localeCompare(titleB);
if (comp) {
return comp;
}
// secondarily reverse sort by encoding name to sort "windows" or
// "shift_jis" first. This works regardless of localization, because
// the ids aren't localized.
let idA = a.getAttribute("id");
let idB = b.getAttribute("id");
if (idA < idB) {
return 1;
}
if (idB < idA) {
return -1;
}
return 0;
});
for (let item of list) {
parent.appendChild(item);
}
},
});

View file

@ -0,0 +1,251 @@
/* 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/. */
/*
* Handles the validation callback from nsIFormFillController and
* the display of the help panel on invalid elements.
*/
"use strict";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement;
this.EXPORTED_SYMBOLS = [ "FormSubmitObserver" ];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/BrowserUtils.jsm");
function FormSubmitObserver(aWindow, aTabChildGlobal) {
this.init(aWindow, aTabChildGlobal);
}
FormSubmitObserver.prototype =
{
_validationMessage: "",
_content: null,
_element: null,
/*
* Public apis
*/
init: function(aWindow, aTabChildGlobal)
{
this._content = aWindow;
this._tab = aTabChildGlobal;
this._mm =
this._content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDocShell)
.sameTypeRootTreeItem
.QueryInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
// nsIFormSubmitObserver callback about invalid forms. See HTMLFormElement
// for details.
Services.obs.addObserver(this, "invalidformsubmit", false);
this._tab.addEventListener("pageshow", this, false);
this._tab.addEventListener("unload", this, false);
},
uninit: function()
{
Services.obs.removeObserver(this, "invalidformsubmit");
this._content.removeEventListener("pageshow", this, false);
this._content.removeEventListener("unload", this, false);
this._mm = null;
this._element = null;
this._content = null;
this._tab = null;
},
/*
* Events
*/
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "pageshow":
if (this._isRootDocumentEvent(aEvent)) {
this._hidePopup();
}
break;
case "unload":
this.uninit();
break;
case "input":
this._onInput(aEvent);
break;
case "blur":
this._onBlur(aEvent);
break;
}
},
/*
* nsIFormSubmitObserver
*/
notifyInvalidSubmit : function (aFormElement, aInvalidElements)
{
// We are going to handle invalid form submission attempt by focusing the
// first invalid element and show the corresponding validation message in a
// panel attached to the element.
if (!aInvalidElements.length) {
return;
}
// Insure that this is the FormSubmitObserver associated with the form
// element / window this notification is about.
if (this._content != aFormElement.ownerDocument.defaultView.top.document.defaultView) {
return;
}
let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
if (!(element instanceof HTMLInputElement ||
element instanceof HTMLTextAreaElement ||
element instanceof HTMLSelectElement ||
element instanceof HTMLButtonElement)) {
return;
}
// Don't connect up to the same element more than once.
if (this._element == element) {
this._showPopup(element);
return;
}
this._element = element;
element.focus();
this._validationMessage = element.validationMessage;
// Watch for input changes which may change the validation message.
element.addEventListener("input", this, false);
// Watch for focus changes so we can disconnect our listeners and
// hide the popup.
element.addEventListener("blur", this, false);
this._showPopup(element);
},
/*
* Internal
*/
/*
* Handles input changes on the form element we've associated a popup
* with. Updates the validation message or closes the popup if form data
* becomes valid.
*/
_onInput: function (aEvent) {
let element = aEvent.originalTarget;
// If the form input is now valid, hide the popup.
if (element.validity.valid) {
this._hidePopup();
return;
}
// If the element is still invalid for a new reason, we should update
// the popup error message.
if (this._validationMessage != element.validationMessage) {
this._validationMessage = element.validationMessage;
this._showPopup(element);
}
},
/*
* Blur event handler in which we disconnect from the form element and
* hide the popup.
*/
_onBlur: function (aEvent) {
aEvent.originalTarget.removeEventListener("input", this, false);
aEvent.originalTarget.removeEventListener("blur", this, false);
this._element = null;
this._hidePopup();
},
/*
* Send the show popup message to chrome with appropriate position
* information. Can be called repetitively to update the currently
* displayed popup position and text.
*/
_showPopup: function (aElement) {
// Collect positional information and show the popup
let panelData = {};
panelData.message = this._validationMessage;
// Note, this is relative to the browser and needs to be translated
// in chrome.
panelData.contentRect = this._msgRect(aElement);
// We want to show the popup at the middle of checkbox and radio buttons
// and where the content begin for the other elements.
let offset = 0;
let position = "";
if (aElement.tagName == 'INPUT' &&
(aElement.type == 'radio' || aElement.type == 'checkbox')) {
panelData.position = "bottomcenter topleft";
} else {
let win = aElement.ownerDocument.defaultView;
let style = win.getComputedStyle(aElement, null);
if (style.direction == 'rtl') {
offset = parseInt(style.paddingRight) + parseInt(style.borderRightWidth);
} else {
offset = parseInt(style.paddingLeft) + parseInt(style.borderLeftWidth);
}
let zoomFactor = this._getWindowUtils().fullZoom;
panelData.offset = Math.round(offset * zoomFactor);
panelData.position = "after_start";
}
this._mm.sendAsyncMessage("FormValidation:ShowPopup", panelData);
},
_hidePopup: function () {
this._mm.sendAsyncMessage("FormValidation:HidePopup", {});
},
_getWindowUtils: function () {
return this._content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
},
_isRootDocumentEvent: function (aEvent) {
if (this._content == null) {
return true;
}
let target = aEvent.originalTarget;
return (target == this._content.document ||
(target.ownerDocument && target.ownerDocument == this._content.document));
},
/*
* Return a message manager rect for the element's bounding client rect
* in top level browser coords.
*/
_msgRect: function (aElement) {
let domRect = aElement.getBoundingClientRect();
let zoomFactor = this._getWindowUtils().fullZoom;
let { offsetX, offsetY } = BrowserUtils.offsetToTopLevelWindow(this._content, aElement);
return {
left: (domRect.left + offsetX) * zoomFactor,
top: (domRect.top + offsetY) * zoomFactor,
width: domRect.width * zoomFactor,
height: domRect.height * zoomFactor
};
},
QueryInterface : XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver])
};

View file

@ -0,0 +1,157 @@
/* 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/. */
/*
* Chrome side handling of form validation popup.
*/
"use strict";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
this.EXPORTED_SYMBOLS = [ "FormValidationHandler" ];
Cu.import("resource://gre/modules/Services.jsm");
let FormValidationHandler =
{
_panel: null,
_anchor: null,
/*
* Public apis
*/
init: function () {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.addMessageListener("FormValidation:ShowPopup", this);
mm.addMessageListener("FormValidation:HidePopup", this);
},
uninit: function () {
let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
mm.removeMessageListener("FormValidation:ShowPopup", this);
mm.removeMessageListener("FormValidation:HidePopup", this);
this._panel = null;
this._anchor = null;
},
hidePopup: function () {
this._hidePopup();
},
/*
* Events
*/
receiveMessage: function (aMessage) {
let window = aMessage.target.ownerDocument.defaultView;
let json = aMessage.json;
let tabBrowser = window.gBrowser;
switch (aMessage.name) {
case "FormValidation:ShowPopup":
// target is the <browser>, make sure we're receiving a message
// from the foreground tab.
if (tabBrowser && aMessage.target != tabBrowser.selectedBrowser) {
return;
}
this._showPopup(window, json);
break;
case "FormValidation:HidePopup":
this._hidePopup();
break;
}
},
observe: function (aSubject, aTopic, aData) {
this._hidePopup();
},
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "FullZoomChange":
case "TextZoomChange":
case "ZoomChangeUsingMouseWheel":
case "scroll":
this._hidePopup();
break;
case "popuphiding":
this._onPopupHiding(aEvent);
break;
}
},
/*
* Internal
*/
_onPopupHiding: function (aEvent) {
aEvent.originalTarget.removeEventListener("popuphiding", this, true);
let tabBrowser = aEvent.originalTarget.ownerDocument.getElementById("content");
tabBrowser.selectedBrowser.removeEventListener("scroll", this, true);
tabBrowser.selectedBrowser.removeEventListener("FullZoomChange", this, false);
tabBrowser.selectedBrowser.removeEventListener("TextZoomChange", this, false);
tabBrowser.selectedBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this, false);
this._panel.hidden = true;
this._panel = null;
this._anchor.hidden = true;
this._anchor = null;
},
/*
* Shows the form validation popup at a specified position or updates the
* messaging and position if the popup is already displayed.
*
* @aWindow - the chrome window
* @aPanelData - Object that contains popup information
* aPanelData stucture detail:
* contentRect - the bounding client rect of the target element. If
* content is remote, this is relative to the browser, otherwise its
* relative to the window.
* position - popup positional string constants.
* message - the form element validation message text.
*/
_showPopup: function (aWindow, aPanelData) {
let previouslyShown = !!this._panel;
this._panel = aWindow.document.getElementById("invalid-form-popup");
this._panel.firstChild.textContent = aPanelData.message;
this._panel.hidden = false;
let tabBrowser = aWindow.gBrowser;
this._anchor = tabBrowser.formValidationAnchor;
this._anchor.left = aPanelData.contentRect.left;
this._anchor.top = aPanelData.contentRect.top;
this._anchor.width = aPanelData.contentRect.width;
this._anchor.height = aPanelData.contentRect.height;
this._anchor.hidden = false;
// Display the panel if it isn't already visible.
if (!previouslyShown) {
// Cleanup after the popup is hidden
this._panel.addEventListener("popuphiding", this, true);
// Hide if the user scrolls the page
tabBrowser.selectedBrowser.addEventListener("scroll", this, true);
tabBrowser.selectedBrowser.addEventListener("FullZoomChange", this, false);
tabBrowser.selectedBrowser.addEventListener("TextZoomChange", this, false);
tabBrowser.selectedBrowser.addEventListener("ZoomChangeUsingMouseWheel", this, false);
// Open the popup
this._panel.openPopup(this._anchor, aPanelData.position, 0, 0, false);
}
},
/*
* Hide the popup if currently displayed. Will fire an event to onPopupHiding
* above if visible.
*/
_hidePopup: function () {
if (this._panel) {
this._panel.hidePopup();
}
}
};

View file

@ -0,0 +1,179 @@
/* 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/. */
/*
* This module adjusts network priority for tabs in a way that gives 'important'
* tabs a higher priority. There are 3 levels of priority. Each is listed below
* with the priority adjustment used.
*
* Highest (-10): Selected tab in the focused window.
* Medium (0): Background tabs in the focused window.
* Selected tab in background windows.
* Lowest (+10): Background tabs in background windows.
*/
this.EXPORTED_SYMBOLS = ["trackBrowserWindow"];
const Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
// Lazy getters
XPCOMUtils.defineLazyServiceGetter(this, "_focusManager",
"@mozilla.org/focus-manager;1",
"nsIFocusManager");
// Constants
const TAB_EVENTS = ["TabOpen", "TabSelect"];
const WINDOW_EVENTS = ["activate", "unload"];
// PRIORITY DELTA is -10 because lower priority value is actually a higher priority
const PRIORITY_DELTA = -10;
// Variables
let _lastFocusedWindow = null;
let _windows = [];
// Exported symbol
this.trackBrowserWindow = function trackBrowserWindow(aWindow) {
WindowHelper.addWindow(aWindow);
}
// Global methods
function _handleEvent(aEvent) {
switch (aEvent.type) {
case "TabOpen":
BrowserHelper.onOpen(aEvent.target.linkedBrowser);
break;
case "TabSelect":
BrowserHelper.onSelect(aEvent.target.linkedBrowser);
break;
case "activate":
WindowHelper.onActivate(aEvent.target);
break;
case "unload":
WindowHelper.removeWindow(aEvent.currentTarget);
break;
}
}
// Methods that impact a browser. Put into single object for organization.
let BrowserHelper = {
onOpen: function NP_BH_onOpen(aBrowser) {
// If the tab is in the focused window, leave priority as it is
if (aBrowser.ownerDocument.defaultView != _lastFocusedWindow)
this.decreasePriority(aBrowser);
},
onSelect: function NP_BH_onSelect(aBrowser) {
let windowEntry = WindowHelper.getEntry(aBrowser.ownerDocument.defaultView);
if (windowEntry.lastSelectedBrowser)
this.decreasePriority(windowEntry.lastSelectedBrowser);
this.increasePriority(aBrowser);
windowEntry.lastSelectedBrowser = aBrowser;
},
increasePriority: function NP_BH_increasePriority(aBrowser) {
aBrowser.adjustPriority(PRIORITY_DELTA);
},
decreasePriority: function NP_BH_decreasePriority(aBrowser) {
aBrowser.adjustPriority(PRIORITY_DELTA * -1);
}
};
// Methods that impact a window. Put into single object for organization.
let WindowHelper = {
addWindow: function NP_WH_addWindow(aWindow) {
// Build internal data object
_windows.push({ window: aWindow, lastSelectedBrowser: null });
// Add event listeners
TAB_EVENTS.forEach(function(event) {
aWindow.gBrowser.tabContainer.addEventListener(event, _handleEvent, false);
});
WINDOW_EVENTS.forEach(function(event) {
aWindow.addEventListener(event, _handleEvent, false);
});
// This gets called AFTER activate event, so if this is the focused window
// we want to activate it. Otherwise, deprioritize it.
if (aWindow == _focusManager.activeWindow)
this.handleFocusedWindow(aWindow);
else
this.decreasePriority(aWindow);
// Select the selected tab
BrowserHelper.onSelect(aWindow.gBrowser.selectedBrowser);
},
removeWindow: function NP_WH_removeWindow(aWindow) {
if (aWindow == _lastFocusedWindow)
_lastFocusedWindow = null;
// Delete this window from our tracking
_windows.splice(this.getEntryIndex(aWindow), 1);
// Remove the event listeners
TAB_EVENTS.forEach(function(event) {
aWindow.gBrowser.tabContainer.removeEventListener(event, _handleEvent, false);
});
WINDOW_EVENTS.forEach(function(event) {
aWindow.removeEventListener(event, _handleEvent, false);
});
},
onActivate: function NP_WH_onActivate(aWindow, aHasFocus) {
// If this window was the last focused window, we don't need to do anything
if (aWindow == _lastFocusedWindow)
return;
// handleFocusedWindow will deprioritize the current window
this.handleFocusedWindow(aWindow);
// Lastly we should increase priority for this window
this.increasePriority(aWindow);
},
handleFocusedWindow: function NP_WH_handleFocusedWindow(aWindow) {
// If we have a last focused window, we need to deprioritize it first
if (_lastFocusedWindow)
this.decreasePriority(_lastFocusedWindow);
// aWindow is now focused
_lastFocusedWindow = aWindow;
},
// Auxiliary methods
increasePriority: function NP_WH_increasePriority(aWindow) {
aWindow.gBrowser.browsers.forEach(function(aBrowser) {
BrowserHelper.increasePriority(aBrowser);
});
},
decreasePriority: function NP_WH_decreasePriority(aWindow) {
aWindow.gBrowser.browsers.forEach(function(aBrowser) {
BrowserHelper.decreasePriority(aBrowser);
});
},
getEntry: function NP_WH_getEntry(aWindow) {
return _windows[this.getEntryIndex(aWindow)];
},
getEntryIndex: function NP_WH_getEntryAtIndex(aWindow) {
// Assumes that every object has a unique window & it's in the array
for (let i = 0; i < _windows.length; i++)
if (_windows[i].window == aWindow)
return i;
}
};

View file

@ -0,0 +1,238 @@
/* 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/. */
this.EXPORTED_SYMBOLS = ["PageMenu"];
this.PageMenu = function PageMenu() {
}
PageMenu.prototype = {
PAGEMENU_ATTR: "pagemenu",
GENERATEDITEMID_ATTR: "generateditemid",
_popup: null,
_builder: null,
// Given a target node, get the context menu for it or its ancestor.
getContextMenu: function(aTarget) {
let target = aTarget;
while (target) {
let contextMenu = target.contextMenu;
if (contextMenu) {
return contextMenu;
}
target = target.parentNode;
}
return null;
},
// Given a target node, generate a JSON object for any context menu
// associated with it, or null if there is no context menu.
maybeBuild: function(aTarget) {
let pageMenu = this.getContextMenu(aTarget);
if (!pageMenu) {
return null;
}
pageMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
pageMenu.sendShowEvent();
// the show event is not cancelable, so no need to check a result here
this._builder = pageMenu.createBuilder();
if (!this._builder) {
return null;
}
pageMenu.build(this._builder);
// This serializes then parses again, however this could be avoided in
// the single-process case with further improvement.
let menuString = this._builder.toJSONString();
if (!menuString) {
return null;
}
return JSON.parse(menuString);
},
// Given a JSON menu object and popup, add the context menu to the popup.
buildAndAttachMenuWithObject: function(aMenu, aBrowser, aPopup) {
if (!aMenu) {
return false;
}
let insertionPoint = this.getInsertionPoint(aPopup);
if (!insertionPoint) {
return false;
}
let fragment = aPopup.ownerDocument.createDocumentFragment();
this.buildXULMenu(aMenu, fragment);
let pos = insertionPoint.getAttribute(this.PAGEMENU_ATTR);
if (pos == "start") {
insertionPoint.insertBefore(fragment,
insertionPoint.firstChild);
} else if (pos.startsWith("#")) {
insertionPoint.insertBefore(fragment, insertionPoint.querySelector(pos));
} else {
insertionPoint.appendChild(fragment);
}
this._popup = aPopup;
this._popup.addEventListener("command", this);
this._popup.addEventListener("popuphidden", this);
return true;
},
// Construct the XUL menu structure for a given JSON object.
buildXULMenu: function(aNode, aElementForAppending) {
let document = aElementForAppending.ownerDocument;
let children = aNode.children;
for (let child of children) {
let menuitem;
switch (child.type) {
case "menuitem":
if (!child.id) {
continue; // Ignore children without ids
}
menuitem = document.createElement("menuitem");
if (child.checkbox) {
menuitem.setAttribute("type", "checkbox");
if (child.checked) {
menuitem.setAttribute("checked", "true");
}
}
if (child.label) {
menuitem.setAttribute("label", child.label);
}
if (child.icon) {
menuitem.setAttribute("image", child.icon);
menuitem.className = "menuitem-iconic";
}
if (child.disabled) {
menuitem.setAttribute("disabled", true);
}
break;
case "separator":
menuitem = document.createElement("menuseparator");
break;
case "menu":
menuitem = document.createElement("menu");
if (child.label) {
menuitem.setAttribute("label", child.label);
}
let menupopup = document.createElement("menupopup");
menuitem.appendChild(menupopup);
this.buildXULMenu(child, menupopup);
break;
}
menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0);
aElementForAppending.appendChild(menuitem);
}
},
// Called when the generated menuitem is executed.
handleEvent: function(event) {
let type = event.type;
let target = event.target;
if (type == "command" && target.hasAttribute(this.GENERATEDITEMID_ATTR)) {
// If a builder is assigned, call click on it directly. Otherwise, this is
// likely a menu with data from another process, so send a message to the
// browser to execute the menuitem.
if (this._builder) {
this._builder.click(target.getAttribute(this.GENERATEDITEMID_ATTR));
}
} else if (type == "popuphidden" && this._popup == target) {
this.removeGeneratedContent(this._popup);
this._popup.removeEventListener("popuphidden", this);
this._popup.removeEventListener("command", this);
this._popup = null;
this._builder = null;
}
},
// Get the first child of the given element with the given tag name.
getImmediateChild: function(element, tag) {
let child = element.firstChild;
while (child) {
if (child.localName == tag) {
return child;
}
child = child.nextSibling;
}
return null;
},
// Return the location where the generated items should be inserted into the
// given popup. They should be inserted as the next sibling of the returned
// element.
getInsertionPoint: function(aPopup) {
if (aPopup.hasAttribute(this.PAGEMENU_ATTR))
return aPopup;
let element = aPopup.firstChild;
while (element) {
if (element.localName == "menu") {
let popup = this.getImmediateChild(element, "menupopup");
if (popup) {
let result = this.getInsertionPoint(popup);
if (result) {
return result;
}
}
}
element = element.nextSibling;
}
return null;
},
// Returns true if custom menu items were present.
maybeBuildAndAttachMenu: function(aTarget, aPopup) {
let menuObject = this.maybeBuild(aTarget);
if (!menuObject) {
return false;
}
return this.buildAndAttachMenuWithObject(menuObject, null, aPopup);
},
// Remove the generated content from the given popup.
removeGeneratedContent: function(aPopup) {
let ungenerated = [];
ungenerated.push(aPopup);
let count;
while (0 != (count = ungenerated.length)) {
let last = count - 1;
let element = ungenerated[last];
ungenerated.splice(last, 1);
let i = element.childNodes.length;
while (i-- > 0) {
let child = element.childNodes[i];
if (!child.hasAttribute(this.GENERATEDITEMID_ATTR)) {
ungenerated.push(child);
continue;
}
element.removeChild(child);
}
}
}
}

View file

@ -0,0 +1,843 @@
/* 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/. */
this.EXPORTED_SYMBOLS = ["PopupNotifications"];
var Cc = Components.classes, Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/Services.jsm");
const NOTIFICATION_EVENT_DISMISSED = "dismissed";
const NOTIFICATION_EVENT_REMOVED = "removed";
const NOTIFICATION_EVENT_SHOWING = "showing";
const NOTIFICATION_EVENT_SHOWN = "shown";
const ICON_SELECTOR = ".notification-anchor-icon";
const ICON_ATTRIBUTE_SHOWING = "showing";
const PREF_SECURITY_DELAY = "security.notification_enable_delay";
let popupNotificationsMap = new WeakMap();
let gNotificationParents = new WeakMap;
function getAnchorFromBrowser(aBrowser) {
let anchor = aBrowser.getAttribute("popupnotificationanchor") ||
aBrowser.popupnotificationanchor;
if (anchor) {
if (anchor instanceof Ci.nsIDOMXULElement) {
return anchor;
}
return aBrowser.ownerDocument.getElementById(anchor);
}
return null;
}
/**
* Notification object describes a single popup notification.
*
* @see PopupNotifications.show()
*/
function Notification(id, message, anchorID, mainAction, secondaryActions,
browser, owner, options) {
this.id = id;
this.message = message;
this.anchorID = anchorID;
this.mainAction = mainAction;
this.secondaryActions = secondaryActions || [];
this.browser = browser;
this.owner = owner;
this.options = options || {};
}
Notification.prototype = {
id: null,
message: null,
anchorID: null,
mainAction: null,
secondaryActions: null,
browser: null,
owner: null,
options: null,
timeShown: null,
/**
* Removes the notification and updates the popup accordingly if needed.
*/
remove: function Notification_remove() {
this.owner.remove(this);
},
get anchorElement() {
let iconBox = this.owner.iconBox;
let anchorElement = getAnchorFromBrowser(this.browser);
if (!iconBox)
return anchorElement;
if (!anchorElement && this.anchorID)
anchorElement = iconBox.querySelector("#"+this.anchorID);
// Use a default anchor icon if it's available
if (!anchorElement)
anchorElement = iconBox.querySelector("#default-notification-icon") ||
iconBox;
return anchorElement;
},
reshow: function() {
this.owner._reshowNotifications(this.anchorElement, this.browser);
}
};
/**
* The PopupNotifications object manages popup notifications for a given browser
* window.
* @param tabbrowser
* window's <xul:tabbrowser/>. Used to observe tab switching events and
* for determining the active browser element.
* @param panel
* The <xul:panel/> element to use for notifications. The panel is
* populated with <popupnotification> children and displayed it as
* needed.
* @param iconBox
* Reference to a container element that should be hidden or
* unhidden when notifications are hidden or shown. It should be the
* parent of anchor elements whose IDs are passed to show().
* It is used as a fallback popup anchor if notifications specify
* invalid or non-existent anchor IDs.
*/
this.PopupNotifications = function PopupNotifications(tabbrowser, panel, iconBox) {
if (!(tabbrowser instanceof Ci.nsIDOMXULElement))
throw "Invalid tabbrowser";
if (iconBox && !(iconBox instanceof Ci.nsIDOMXULElement))
throw "Invalid iconBox";
if (!(panel instanceof Ci.nsIDOMXULElement))
throw "Invalid panel";
this.window = tabbrowser.ownerDocument.defaultView;
this.panel = panel;
this.tabbrowser = tabbrowser;
this.iconBox = iconBox;
this.buttonDelay = Services.prefs.getIntPref(PREF_SECURITY_DELAY);
this.panel.addEventListener("popuphidden", this, true);
this.window.addEventListener("activate", this, true);
if (this.tabbrowser.tabContainer)
this.tabbrowser.tabContainer.addEventListener("TabSelect", this, true);
}
PopupNotifications.prototype = {
window: null,
panel: null,
tabbrowser: null,
_iconBox: null,
set iconBox(iconBox) {
// Remove the listeners on the old iconBox, if needed
if (this._iconBox) {
this._iconBox.removeEventListener("click", this, false);
this._iconBox.removeEventListener("keypress", this, false);
}
this._iconBox = iconBox;
if (iconBox) {
iconBox.addEventListener("click", this, false);
iconBox.addEventListener("keypress", this, false);
}
},
get iconBox() {
return this._iconBox;
},
/**
* Retrieve a Notification object associated with the browser/ID pair.
* @param id
* The Notification ID to search for.
* @param browser
* The browser whose notifications should be searched. If null, the
* currently selected browser's notifications will be searched.
*
* @returns the corresponding Notification object, or null if no such
* notification exists.
*/
getNotification: function PopupNotifications_getNotification(id, browser) {
let n = null;
let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
notifications.some(function(x) x.id == id && (n = x));
return n;
},
/**
* Adds a new popup notification.
* @param browser
* The <xul:browser> element associated with the notification. Must not
* be null.
* @param id
* A unique ID that identifies the type of notification (e.g.
* "geolocation"). Only one notification with a given ID can be visible
* at a time. If a notification already exists with the given ID, it
* will be replaced.
* @param message
* The text to be displayed in the notification.
* @param anchorID
* The ID of the element that should be used as this notification
* popup's anchor. May be null, in which case the notification will be
* anchored to the iconBox.
* @param mainAction
* A JavaScript object literal describing the notification button's
* action. If present, it must have the following properties:
* - label (string): the button's label.
* - accessKey (string): the button's accessKey.
* - callback (function): a callback to be invoked when the button is
* pressed.
* If null, the notification will not have a button, and
* secondaryActions will be ignored.
* @param secondaryActions
* An optional JavaScript array describing the notification's alternate
* actions. The array should contain objects with the same properties
* as mainAction. These are used to populate the notification button's
* dropdown menu.
* @param options
* An options JavaScript object holding additional properties for the
* notification. The following properties are currently supported:
* persistence: An integer. The notification will not automatically
* dismiss for this many page loads.
* timeout: A time in milliseconds. The notification will not
* automatically dismiss before this time.
* persistWhileVisible:
* A boolean. If true, a visible notification will always
* persist across location changes.
* dismissed: Whether the notification should be added as a dismissed
* notification. Dismissed notifications can be activated
* by clicking on their anchorElement.
* eventCallback:
* Callback to be invoked when the notification changes
* state. The callback's first argument is a string
* identifying the state change:
* "dismissed": notification has been dismissed by the
* user (e.g. by clicking away or switching
* tabs)
* "removed": notification has been removed (due to
* location change or user action)
* "shown": notification has been shown (this can be fired
* multiple times as notifications are dismissed
* and re-shown)
* neverShow: Indicate that no popup should be shown for this
* notification. Useful for just showing the anchor icon.
* removeOnDismissal:
* Notifications with this parameter set to true will be
* removed when they would have otherwise been dismissed
* (i.e. any time the popup is closed due to user
* interaction).
* popupIconURL:
* A string. URL of the image to be displayed in the popup.
* Normally specified in CSS using list-style-image and the
* .popup-notification-icon[popupid=...] selector.
* learnMoreURL:
* A string URL. Setting this property will make the
* prompt display a "Learn More" link that, when clicked,
* opens the URL in a new tab.
* @returns the Notification object corresponding to the added notification.
*/
show: function PopupNotifications_show(browser, id, message, anchorID,
mainAction, secondaryActions, options) {
function isInvalidAction(a) {
return !a || !(typeof(a.callback) == "function") || !a.label || !a.accessKey;
}
if (!browser)
throw "PopupNotifications_show: invalid browser";
if (!id)
throw "PopupNotifications_show: invalid ID";
if (mainAction && isInvalidAction(mainAction))
throw "PopupNotifications_show: invalid mainAction";
if (secondaryActions && secondaryActions.some(isInvalidAction))
throw "PopupNotifications_show: invalid secondaryActions";
let notification = new Notification(id, message, anchorID, mainAction,
secondaryActions, browser, this, options);
if (options && options.dismissed)
notification.dismissed = true;
let existingNotification = this.getNotification(id, browser);
if (existingNotification)
this._remove(existingNotification);
let notifications = this._getNotificationsForBrowser(browser);
notifications.push(notification);
let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
if (browser.docShell.isActive && fm.activeWindow == this.window) {
// show panel now
this._update(notifications, notification.anchorElement, true);
} else {
// Otherwise, update() will display the notification the next time the
// relevant tab/window is selected.
// If the tab is selected but the window is in the background, let the OS
// tell the user that there's a notification waiting in that window.
// At some point we might want to do something about background tabs here
// too. When the user switches to this window, we'll show the panel if
// this browser is a tab (thus showing the anchor icon). For
// non-tabbrowser browsers, we need to make the icon visible now or the
// user will not be able to open the panel.
if (!notification.dismissed && browser.docShell.isActive) {
this.window.getAttention();
if (notification.anchorElement.parentNode != this.iconBox) {
notification.anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
}
}
// Notify observers that we're not showing the popup (useful for testing)
this._notify("backgroundShow");
}
return notification;
},
/**
* Returns true if the notification popup is currently being displayed.
*/
get isPanelOpen() {
let panelState = this.panel.state;
return panelState == "showing" || panelState == "open";
},
/**
* Called by the consumer to indicate that a browser's location has changed,
* so that we can update the active notifications accordingly.
*/
locationChange: function PopupNotifications_locationChange(aBrowser) {
if (!aBrowser)
throw "PopupNotifications_locationChange: invalid browser";
let notifications = this._getNotificationsForBrowser(aBrowser);
notifications = notifications.filter(function (notification) {
// The persistWhileVisible option allows an open notification to persist
// across location changes
if (notification.options.persistWhileVisible &&
this.isPanelOpen) {
if ("persistence" in notification.options &&
notification.options.persistence)
notification.options.persistence--;
return true;
}
// The persistence option allows a notification to persist across multiple
// page loads
if ("persistence" in notification.options &&
notification.options.persistence) {
notification.options.persistence--;
return true;
}
// The timeout option allows a notification to persist until a certain time
if ("timeout" in notification.options &&
Date.now() <= notification.options.timeout) {
return true;
}
this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
return false;
}, this);
this._setNotificationsForBrowser(aBrowser, notifications);
if (aBrowser.docShell.isActive) {
// get the anchor element if the browser has defined one so it will
// _update will handle both the tabs iconBox and non-tab permission
// anchors.
let anchorElement = notifications.length > 0 ? notifications[0].anchorElement : null;
if (!anchorElement)
anchorElement = getAnchorFromBrowser(aBrowser);
this._update(notifications, anchorElement);
}
},
/**
* Removes a Notification.
* @param notification
* The Notification object to remove.
*/
remove: function PopupNotifications_remove(notification) {
this._remove(notification);
if (notification.browser.docShell.isActive) {
let notifications = this._getNotificationsForBrowser(notification.browser);
this._update(notifications, notification.anchorElement);
}
},
handleEvent: function (aEvent) {
switch (aEvent.type) {
case "popuphidden":
this._onPopupHidden(aEvent);
break;
case "activate":
case "TabSelect":
let self = this;
// setTimeout(..., 0) needed, otherwise openPopup from "activate" event
// handler results in the popup being hidden again for some reason...
this.window.setTimeout(function () {
self._update();
}, 0);
break;
case "click":
case "keypress":
this._onIconBoxCommand(aEvent);
break;
}
},
////////////////////////////////////////////////////////////////////////////////
// Utility methods
////////////////////////////////////////////////////////////////////////////////
_ignoreDismissal: null,
_currentAnchorElement: null,
/**
* Gets notifications for the currently selected browser.
*/
get _currentNotifications() {
return this.tabbrowser.selectedBrowser ? this._getNotificationsForBrowser(this.tabbrowser.selectedBrowser) : [];
},
_remove: function PopupNotifications_removeHelper(notification) {
// This notification may already be removed, in which case let's just fail
// silently.
let notifications = this._getNotificationsForBrowser(notification.browser);
if (!notifications)
return;
var index = notifications.indexOf(notification);
if (index == -1)
return;
if (notification.browser.docShell.isActive)
notification.anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
// remove the notification
notifications.splice(index, 1);
this._fireCallback(notification, NOTIFICATION_EVENT_REMOVED);
},
/**
* Dismisses the notification without removing it.
*/
_dismiss: function PopupNotifications_dismiss() {
let browser = this.panel.firstChild &&
this.panel.firstChild.notification.browser;
if (typeof this.panel.hidePopup === "function") {
this.panel.hidePopup();
}
if (browser)
browser.focus();
},
/**
* Hides the notification popup.
*/
_hidePanel: function PopupNotifications_hide() {
this._ignoreDismissal = true;
if (typeof this.panel.hidePopup === "function") {
this.panel.hidePopup();
}
this._ignoreDismissal = false;
},
/**
* Removes all notifications from the notification popup.
*/
_clearPanel: function () {
let popupnotification;
while ((popupnotification = this.panel.lastChild)) {
this.panel.removeChild(popupnotification);
// If this notification was provided by the chrome document rather than
// created ad hoc, move it back to where we got it from.
let originalParent = gNotificationParents.get(popupnotification);
if (originalParent) {
popupnotification.notification = null;
// Remove nodes dynamically added to the notification's menu button
// in _refreshPanel. Keep popupnotificationcontent nodes; they are
// provided by the chrome document.
let contentNode = popupnotification.lastChild;
while (contentNode) {
let previousSibling = contentNode.previousSibling;
if (contentNode.nodeName != "popupnotificationcontent")
popupnotification.removeChild(contentNode);
contentNode = previousSibling;
}
// Re-hide the notification such that it isn't rendered in the chrome
// document. _refreshPanel will unhide it again when needed.
popupnotification.hidden = true;
originalParent.appendChild(popupnotification);
}
}
},
_refreshPanel: function PopupNotifications_refreshPanel(notificationsToShow) {
this._clearPanel();
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
notificationsToShow.forEach(function (n) {
let doc = this.window.document;
// Append "-notification" to the ID to try to avoid ID conflicts with other stuff
// in the document.
let popupnotificationID = n.id + "-notification";
// If the chrome document provides a popupnotification with this id, use
// that. Otherwise create it ad-hoc.
let popupnotification = doc.getElementById(popupnotificationID);
if (popupnotification)
gNotificationParents.set(popupnotification, popupnotification.parentNode);
else
popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
popupnotification.setAttribute("label", n.message);
popupnotification.setAttribute("id", popupnotificationID);
popupnotification.setAttribute("popupid", n.id);
popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
if (n.mainAction) {
popupnotification.setAttribute("buttonlabel", n.mainAction.label);
popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
} else {
popupnotification.removeAttribute("buttonlabel");
popupnotification.removeAttribute("buttonaccesskey");
popupnotification.removeAttribute("buttoncommand");
popupnotification.removeAttribute("menucommand");
popupnotification.removeAttribute("closeitemcommand");
}
if (n.options.popupIconURL)
popupnotification.setAttribute("icon", n.options.popupIconURL);
if (n.options.learnMoreURL)
popupnotification.setAttribute("learnmoreurl", n.options.learnMoreURL);
else
popupnotification.removeAttribute("learnmoreurl");
popupnotification.notification = n;
if (n.secondaryActions) {
n.secondaryActions.forEach(function (a) {
let item = doc.createElementNS(XUL_NS, "menuitem");
item.setAttribute("label", a.label);
item.setAttribute("accesskey", a.accessKey);
item.notification = n;
item.action = a;
popupnotification.appendChild(item);
}, this);
if (n.secondaryActions.length) {
let closeItemSeparator = doc.createElementNS(XUL_NS, "menuseparator");
popupnotification.appendChild(closeItemSeparator);
}
}
this.panel.appendChild(popupnotification);
// The popupnotification may be hidden if we got it from the chrome
// document rather than creating it ad hoc.
popupnotification.hidden = false;
}, this);
},
_showPanel: function PopupNotifications_showPanel(notificationsToShow, anchorElement) {
this.panel.hidden = false;
notificationsToShow.forEach(function (n) {
this._fireCallback(n, NOTIFICATION_EVENT_SHOWING);
}, this);
this._refreshPanel(notificationsToShow);
if (this.isPanelOpen && this._currentAnchorElement == anchorElement)
return;
// If the panel is already open but we're changing anchors, we need to hide
// it first. Otherwise it can appear in the wrong spot. (_hidePanel is
// safe to call even if the panel is already hidden.)
this._hidePanel();
// If the anchor element is hidden or null, use the tab as the anchor. We
// only ever show notifications for the current browser, so we can just use
// the current tab.
let selectedTab = this.tabbrowser.selectedTab;
if (anchorElement) {
let bo = anchorElement.boxObject;
if (bo.height == 0 && bo.width == 0)
anchorElement = selectedTab; // hidden
} else {
anchorElement = selectedTab; // null
}
this._currentAnchorElement = anchorElement;
// On OS X and Linux we need a different panel arrow color for
// click-to-play plugins, so copy the popupid and use css.
this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
notificationsToShow.forEach(function (n) {
// Remember the time the notification was shown for the security delay.
n.timeShown = this.window.performance.now();
}, this);
this.panel.openPopup(anchorElement, "bottomcenter topleft");
notificationsToShow.forEach(function (n) {
this._fireCallback(n, NOTIFICATION_EVENT_SHOWN);
}, this);
},
/**
* Updates the notification state in response to window activation or tab
* selection changes.
*
* @param notifications an array of Notification instances. if null,
* notifications will be retrieved off the current
* browser tab
* @param anchor is a XUL element that the notifications panel will be
* anchored to
* @param dismissShowing if true, dismiss any currently visible notifications
* if there are no notifications to show. Otherwise,
* currently displayed notifications will be left alone.
*/
_update: function PopupNotifications_update(notifications, anchor, dismissShowing = false) {
let useIconBox = this.iconBox && (!anchor || anchor.parentNode == this.iconBox);
if (useIconBox) {
// hide icons of the previous tab.
this._hideIcons();
}
let anchorElement = anchor, notificationsToShow = [];
if (!notifications)
notifications = this._currentNotifications;
let haveNotifications = notifications.length > 0;
if (haveNotifications) {
// Only show the notifications that have the passed-in anchor (or the
// first notification's anchor, if none was passed in). Other
// notifications will be shown once these are dismissed.
anchorElement = anchor || notifications[0].anchorElement;
if (useIconBox) {
this._showIcons(notifications);
this.iconBox.hidden = false;
} else if (anchorElement) {
anchorElement.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
// use the anchorID as a class along with the default icon class as a
// fallback if anchorID is not defined in CSS. We always use the first
// notifications icon, so in the case of multiple notifications we'll
// only use the default icon
if (anchorElement.classList.contains("notification-anchor-icon")) {
// remove previous icon classes
let className = anchorElement.className.replace(/([-\w]+-notification-icon\s?)/g,"")
className = "default-notification-icon " + className;
if (notifications.length == 1) {
className = notifications[0].anchorID + " " + className;
}
anchorElement.className = className;
}
}
// Also filter out notifications that have been dismissed.
notificationsToShow = notifications.filter(function (n) {
return !n.dismissed && n.anchorElement == anchorElement &&
!n.options.neverShow;
});
}
if (notificationsToShow.length > 0) {
this._showPanel(notificationsToShow, anchorElement);
} else {
// Notify observers that we're not showing the popup (useful for testing)
this._notify("updateNotShowing");
// Close the panel if there are no notifications to show.
// When called from PopupNotifications.show() we should never close the
// panel, however. It may just be adding a dismissed notification, in
// which case we want to continue showing any existing notifications.
if (!dismissShowing)
this._dismiss();
// Only hide the iconBox if we actually have no notifications (as opposed
// to not having any showable notifications)
if (!haveNotifications) {
if (useIconBox)
this.iconBox.hidden = true;
else if (anchorElement)
anchorElement.removeAttribute(ICON_ATTRIBUTE_SHOWING);
}
}
},
_showIcons: function PopupNotifications_showIcons(aCurrentNotifications) {
for (let notification of aCurrentNotifications) {
let anchorElm = notification.anchorElement;
if (anchorElm) {
anchorElm.setAttribute(ICON_ATTRIBUTE_SHOWING, "true");
}
}
},
_hideIcons: function PopupNotifications_hideIcons() {
let icons = this.iconBox.querySelectorAll(ICON_SELECTOR);
for (let icon of icons) {
icon.removeAttribute(ICON_ATTRIBUTE_SHOWING);
}
},
/**
* Gets and sets notifications for the browser.
*/
_getNotificationsForBrowser: function PopupNotifications_getNotifications(browser) {
let notifications = popupNotificationsMap.get(browser);
if (!notifications) {
// Initialize the WeakMap for the browser so callers can reference/manipulate the array.
notifications = [];
popupNotificationsMap.set(browser, notifications);
}
return notifications;
},
_setNotificationsForBrowser: function PopupNotifications_setNotifications(browser, notifications) {
popupNotificationsMap.set(browser, notifications);
return notifications;
},
_onIconBoxCommand: function PopupNotifications_onIconBoxCommand(event) {
// Left click, space or enter only
let type = event.type;
if (type == "click" && event.button != 0)
return;
if (type == "keypress" &&
!(event.charCode == Ci.nsIDOMKeyEvent.DOM_VK_SPACE ||
event.keyCode == Ci.nsIDOMKeyEvent.DOM_VK_RETURN))
return;
if (this._currentNotifications.length == 0)
return;
// Get the anchor that is the immediate child of the icon box
let anchor = event.target;
while (anchor && anchor.parentNode != this.iconBox)
anchor = anchor.parentNode;
this._reshowNotifications(anchor);
},
_reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
// Mark notifications anchored to this anchor as un-dismissed
let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
notifications.forEach(function (n) {
if (n.anchorElement == anchor)
n.dismissed = false;
});
// ...and then show them.
this._update(notifications, anchor);
},
_fireCallback: function PopupNotifications_fireCallback(n, event) {
if (n.options.eventCallback)
n.options.eventCallback.call(n, event);
},
_onPopupHidden: function PopupNotifications_onPopupHidden(event) {
if (event.target != this.panel || this._ignoreDismissal)
return;
let browser = this.panel.firstChild &&
this.panel.firstChild.notification.browser;
if (!browser)
return;
let notifications = this._getNotificationsForBrowser(browser);
// Mark notifications as dismissed and call dismissal callbacks
Array.forEach(this.panel.childNodes, function (nEl) {
let notificationObj = nEl.notification;
// Never call a dismissal handler on a notification that's been removed.
if (notifications.indexOf(notificationObj) == -1)
return;
// Do not mark the notification as dismissed or fire NOTIFICATION_EVENT_DISMISSED
// if the notification is removed.
if (notificationObj.options.removeOnDismissal)
this._remove(notificationObj);
else {
notificationObj.dismissed = true;
this._fireCallback(notificationObj, NOTIFICATION_EVENT_DISMISSED);
}
}, this);
this._clearPanel();
this._update();
},
_onButtonCommand: function PopupNotifications_onButtonCommand(event) {
// Need to find the associated notification object, which is a bit tricky
// since it isn't associated with the button directly - this is kind of
// gross and very dependent on the structure of the popupnotification
// binding's content.
let target = event.originalTarget;
let notificationEl;
let parent = target;
while (parent && (parent = target.ownerDocument.getBindingParent(parent)))
notificationEl = parent;
if (!notificationEl)
throw "PopupNotifications_onButtonCommand: couldn't find notification element";
if (!notificationEl.notification)
throw "PopupNotifications_onButtonCommand: couldn't find notification";
let notification = notificationEl.notification;
let timeSinceShown = this.window.performance.now() - notification.timeShown;
// Only report the first time mainAction is triggered and remember that this occurred.
if (!notification.timeMainActionFirstTriggered) {
notification.timeMainActionFirstTriggered = timeSinceShown;
}
if (timeSinceShown < this.buttonDelay) {
Services.console.logStringMessage("PopupNotifications_onButtonCommand: " +
"Button click happened before the security delay: " +
timeSinceShown + "ms");
return;
}
notification.mainAction.callback.call();
this._remove(notification);
this._update();
},
_onMenuCommand: function PopupNotifications_onMenuCommand(event) {
let target = event.originalTarget;
if (!target.action || !target.notification)
throw "menucommand target has no associated action/notification";
event.stopPropagation();
target.action.callback.call();
this._remove(target.notification);
this._update();
},
_notify: function PopupNotifications_notify(topic) {
Services.obs.notifyObservers(null, "PopupNotifications-" + topic, "");
},
};

View file

@ -0,0 +1,45 @@
/* 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/. */
this.EXPORTED_SYMBOLS = ["QuotaManagerHelper"];
Components.utils.import('resource://gre/modules/Services.jsm');
const Cc = Components.classes;
const Ci = Components.interfaces;
this.QuotaManagerHelper = {
clear: function(isShutDown) {
try {
var stord = Services.dirsvc.get("ProfD", Ci.nsIFile);
stord.append("storage");
if (stord.exists() && stord.isDirectory()) {
var doms = {};
for (var stor of ["default", "permanent", "temporary"]) {
var storsubd = stord.clone();
storsubd.append(stor);
if (storsubd.exists() && storsubd.isDirectory()) {
var entries = storsubd.directoryEntries;
while(entries.hasMoreElements()) {
var host, entry = entries.getNext();
entry.QueryInterface(Ci.nsIFile);
if ((host = /^(https?|file)\+\+\+(.+)$/.exec(entry.leafName)) !== null) {
if (isShutDown) {
entry.remove(true);
} else {
doms[host[1] + "://" + host[2]] = true;
}
}
}
}
}
var qm = Cc["@mozilla.org/dom/quota/manager;1"].getService(Ci.nsIQuotaManager);
for (var dom in doms) {
var uri = Services.io.newURI(dom, null, null);
qm.clearStoragesForURI(uri);
}
}
} catch(er) {}
}
};

View file

@ -0,0 +1,68 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["RecentWindow"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
#ifndef XP_WIN
#define BROKEN_WM_Z_ORDER
#endif
this.RecentWindow = {
/*
* Get the most recent browser window.
*
* @param aOptions an object accepting the arguments for the search.
* * private: true to restrict the search to private windows
* only, false to restrict the search to non-private only.
* Omit the property to search in both groups.
* * allowPopups: true if popup windows are permissable.
*/
getMostRecentBrowserWindow: function RW_getMostRecentBrowserWindow(aOptions) {
let checkPrivacy = typeof aOptions == "object" &&
"private" in aOptions;
let allowPopups = typeof aOptions == "object" && !!aOptions.allowPopups;
function isSuitableBrowserWindow(win) {
return (!win.closed &&
(allowPopups || win.toolbar.visible) &&
(!checkPrivacy ||
PrivateBrowsingUtils.permanentPrivateBrowsing ||
PrivateBrowsingUtils.isWindowPrivate(win) == aOptions.private));
}
#ifdef BROKEN_WM_Z_ORDER
let win = Services.wm.getMostRecentWindow("navigator:browser");
// if we're lucky, this isn't a popup, and we can just return this
if (win && !isSuitableBrowserWindow(win)) {
win = null;
let windowList = Services.wm.getEnumerator("navigator:browser");
// this is oldest to newest, so this gets a bit ugly
while (windowList.hasMoreElements()) {
let nextWin = windowList.getNext();
if (isSuitableBrowserWindow(nextWin))
win = nextWin;
}
}
return win;
#else
let windowList = Services.wm.getZOrderDOMWindowEnumerator("navigator:browser", true);
while (windowList.hasMoreElements()) {
let win = windowList.getNext();
if (isSuitableBrowserWindow(win))
return win;
}
return null;
#endif
}
};

View file

@ -0,0 +1,221 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = [ "SharedFrame" ];
const Ci = Components.interfaces;
const Cu = Components.utils;
/**
* The purpose of this module is to create and group various iframe
* elements that are meant to all display the same content and only
* one at a time. This makes it possible to have the content loaded
* only once, while the other iframes can be kept as placeholders to
* quickly move the content to them through the swapFrameLoaders function
* when another one of the placeholder is meant to be displayed.
* */
let Frames = new Map();
/**
* The Frames map is the main data structure that holds information
* about the groups being tracked. Each entry's key is the group name,
* and the object holds information about what is the URL being displayed
* on that group, and what is the active element on the group (the frame that
* holds the loaded content).
* The reference to the activeFrame is a weak reference, which allows the
* frame to go away at any time, and when that happens the module considers that
* there are no active elements in that group. The group can be reactivated
* by changing the URL, calling preload again or adding a new element.
*
*
* Frames = {
* "messages-panel": {
* url: string,
* activeFrame: weakref
* }
* }
*
* Each object on the map is called a _SharedFrameGroup, which is an internal
* class of this module which does not automatically keep track of its state. This
* object should not be used externally, and all control should be handled by the
* module's functions.
*/
function UNLOADED_URL(aStr) "data:text/html;charset=utf-8,<!-- Unloaded frame " + aStr + " -->";
this.SharedFrame = {
/**
* Creates an iframe element and track it as part of the specified group
* The module must create the iframe itself because it needs to do some special
* handling for the element's src attribute.
*
* @param aGroupName the name of the group to which this frame belongs
* @param aParent the parent element to which the frame will be appended to
* @param aFrameAttributes an object with a list of attributes to set in the iframe
* before appending it to the DOM. The "src" attribute has
* special meaning here and if it's not blank it specifies
* the URL that will be initially assigned to this group
* @param aPreload optional, tells if the URL specified in the src attribute
* should be preloaded in the frame being created, in case
* it's not yet preloaded in any other frame of the group.
* This parameter has no meaning if src is blank.
*/
createFrame: function (aGroupName, aParent, aFrameAttributes, aPreload = true) {
let frame = aParent.ownerDocument.createElement("iframe");
for (let [key, val] of Iterator(aFrameAttributes)) {
frame.setAttribute(key, val);
}
let src = aFrameAttributes.src;
if (!src) {
aPreload = false;
}
let group = Frames.get(aGroupName);
if (group) {
// If this group has already been created
if (aPreload && !group.isAlive) {
// If aPreload is set and the group is not already loaded, load it.
// This can happen if:
// - aPreload was not used while creating the previous frames of this group, or
// - the previously active frame went dead in the meantime
group.url = src;
this.preload(aGroupName, frame);
} else {
// If aPreload is not set, or the group is already loaded in a different frame,
// there's not much that we need to do here: just create this frame as an
// inactivate placeholder
frame.setAttribute("src", UNLOADED_URL(aGroupName));
}
} else {
// This is the first time we hear about this group, so let's start tracking it,
// and also preload it if the src attribute was set and aPreload = true
group = new _SharedFrameGroup(src);
Frames.set(aGroupName, group);
if (aPreload) {
this.preload(aGroupName, frame);
} else {
frame.setAttribute("src", UNLOADED_URL(aGroupName));
}
}
aParent.appendChild(frame);
return frame;
},
/**
* Function that moves the loaded content from one active frame to
* another one that is currently a placeholder. If there's no active
* frame in the group, the content is loaded/reloaded.
*
* @param aGroupName the name of the group
* @param aTargetFrame the frame element to which the content should
* be moved to.
*/
setOwner: function (aGroupName, aTargetFrame) {
let group = Frames.get(aGroupName);
let frame = group.activeFrame;
if (frame == aTargetFrame) {
// nothing to do here
return;
}
if (group.isAlive) {
// Move document ownership to the desired frame, and make it the active one
frame.QueryInterface(Ci.nsIFrameLoaderOwner).swapFrameLoaders(aTargetFrame);
group.activeFrame = aTargetFrame;
} else {
// Previous owner was dead, reload the document at the new owner and make it the active one
aTargetFrame.setAttribute("src", group.url);
group.activeFrame = aTargetFrame;
}
},
/**
* Updates the current URL in use by this group, and loads it into the active frame.
*
* @param aGroupName the name of the group
* @param aURL the new url
*/
updateURL: function (aGroupName, aURL) {
let group = Frames.get(aGroupName);
group.url = aURL;
if (group.isAlive) {
group.activeFrame.setAttribute("src", aURL);
}
},
/**
* Loads the group's url into a target frame, if the group doesn't have a currently
* active frame.
*
* @param aGroupName the name of the group
* @param aTargetFrame the frame element which should be made active and
* have the group's content loaded to
*/
preload: function (aGroupName, aTargetFrame) {
let group = Frames.get(aGroupName);
if (!group.isAlive) {
aTargetFrame.setAttribute("src", group.url);
group.activeFrame = aTargetFrame;
}
},
/**
* Tells if a group currently have an active element.
*
* @param aGroupName the name of the group
*/
isGroupAlive: function (aGroupName) {
return Frames.get(aGroupName).isAlive;
},
/**
* Forgets about this group. This function doesn't need to be used
* unless the group's name needs to be reused.
*
* @param aGroupName the name of the group
*/
forgetGroup: function (aGroupName) {
Frames.delete(aGroupName);
}
}
function _SharedFrameGroup(aURL) {
this.url = aURL;
this._activeFrame = null;
}
_SharedFrameGroup.prototype = {
get isAlive() {
let frame = this.activeFrame;
return !!(frame &&
frame.contentDocument &&
frame.contentDocument.location);
},
get activeFrame() {
return this._activeFrame &&
this._activeFrame.get();
},
set activeFrame(aActiveFrame) {
this._activeFrame = aActiveFrame
? Cu.getWeakReference(aActiveFrame)
: null;
}
}

View file

@ -0,0 +1,53 @@
/* 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/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = ["Windows8WindowFrameColor"];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/WindowsRegistry.jsm");
const Windows8WindowFrameColor = {
_windowFrameColor: null,
get_win8: function() {
if (this._windowFrameColor)
return this._windowFrameColor;
let HKCU = Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER;
let dwmKey = "Software\\Microsoft\\Windows\\DWM";
// Window frame base color component values when Color Intensity is at 0.
let frameBaseColor = 217;
let windowFrameColor = WindowsRegistry.readRegKey(HKCU, dwmKey,
"ColorizationColor");
if (windowFrameColor == undefined) {
// Return the default color if unset or colorization not used
return this._windowFrameColor = [frameBaseColor, frameBaseColor, frameBaseColor];
}
// The color returned from the Registry is in decimal form.
let windowFrameColorHex = windowFrameColor.toString(16);
// Zero-pad the number just to make sure that it is 8 digits.
windowFrameColorHex = ("00000000" + windowFrameColorHex).substr(-8);
let windowFrameColorArray = windowFrameColorHex.match(/../g);
let [unused, fgR, fgG, fgB] = windowFrameColorArray.map(function(val) parseInt(val, 16));
let windowFrameColorBalance = WindowsRegistry.readRegKey(HKCU, dwmKey,
"ColorizationColorBalance");
// Default to balance=78 if reg key isn't defined
if (windowFrameColorBalance == undefined) {
windowFrameColorBalance = 78;
}
let alpha = windowFrameColorBalance / 100;
// Alpha-blend the foreground color with the frame base color.
let r = Math.round(fgR * alpha + frameBaseColor * (1 - alpha));
let g = Math.round(fgG * alpha + frameBaseColor * (1 - alpha));
let b = Math.round(fgB * alpha + frameBaseColor * (1 - alpha));
return this._windowFrameColor = [r, g, b];
}
};

View file

@ -0,0 +1,581 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/. */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
/**
* Constants
*/
const Cc = Components.classes;
const Ci = Components.interfaces;
// Stop updating jumplists after some idle time.
const IDLE_TIMEOUT_SECONDS = 5 * 60;
// Prefs
const PREF_TASKBAR_BRANCH = "browser.taskbar.lists.";
const PREF_TASKBAR_ENABLED = "enabled";
const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount";
const PREF_TASKBAR_FREQUENT = "frequent.enabled";
const PREF_TASKBAR_RECENT = "recent.enabled";
const PREF_TASKBAR_TASKS = "tasks.enabled";
const PREF_TASKBAR_REFRESH = "refreshInSeconds";
// Hash keys for pendingStatements.
const LIST_TYPE = {
FREQUENT: 0
, RECENT: 1
}
/**
* Exports
*/
this.EXPORTED_SYMBOLS = [
"WinTaskbarJumpList",
];
/**
* Smart getters
*/
XPCOMUtils.defineLazyGetter(this, "_prefs", function() {
return Services.prefs.getBranch(PREF_TASKBAR_BRANCH);
});
XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
return Services.strings
.createBundle("chrome://browser/locale/taskbar.properties");
});
XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
return PlacesUtils;
});
XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
Components.utils.import("resource://gre/modules/NetUtil.jsm");
return NetUtil;
});
XPCOMUtils.defineLazyServiceGetter(this, "_idle",
"@mozilla.org/widget/idleservice;1",
"nsIIdleService");
XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService",
"@mozilla.org/windows-taskbar;1",
"nsIWinTaskbar");
XPCOMUtils.defineLazyServiceGetter(this, "_winShellService",
"@mozilla.org/browser/shell-service;1",
"nsIWindowsShellService");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
/**
* Global functions
*/
function _getString(name) {
return _stringBundle.GetStringFromName(name);
}
/////////////////////////////////////////////////////////////////////////////
// Task list configuration data object.
var tasksCfg = [
/**
* Task configuration options: title, description, args, iconIndex, open, close.
*
* title - Task title displayed in the list. (strings in the table are temp fillers.)
* description - Tooltip description on the list item.
* args - Command line args to invoke the task.
* iconIndex - Optional win icon index into the main application for the
* list item.
* open - Boolean indicates if the command should be visible after the browser opens.
* close - Boolean indicates if the command should be visible after the browser closes.
*/
// Open new tab
{
get title() _getString("taskbar.tasks.newTab.label"),
get description() _getString("taskbar.tasks.newTab.description"),
args: "-new-tab about:blank",
iconIndex: 3, // New window icon
open: true,
close: true, // The jump list already has an app launch icon, but
// we don't always update the list on shutdown.
// Thus true for consistency.
},
// Open new window
{
get title() _getString("taskbar.tasks.newWindow.label"),
get description() _getString("taskbar.tasks.newWindow.description"),
args: "-browser",
iconIndex: 2, // New tab icon
open: true,
close: true, // No point, but we don't always update the list on
// shutdown. Thus true for consistency.
},
// Open new private window
{
get title() _getString("taskbar.tasks.newPrivateWindow.label"),
get description() _getString("taskbar.tasks.newPrivateWindow.description"),
args: "-private-window",
iconIndex: 4, // Private browsing mode icon
open: true,
close: true, // No point, but we don't always update the list on
// shutdown. Thus true for consistency.
},
];
/////////////////////////////////////////////////////////////////////////////
// Implementation
this.WinTaskbarJumpList =
{
_builder: null,
_tasks: null,
_shuttingDown: false,
/**
* Startup, shutdown, and update
*/
startup: function WTBJL_startup() {
// exit if this isn't win7 or higher.
if (!this._initTaskbar())
return;
// Win shell shortcut maintenance. If we've gone through an update,
// this will update any pinned taskbar shortcuts. Not specific to
// jump lists, but this was a convienent place to call it.
try {
// dev builds may not have helper.exe, ignore failures.
this._shortcutMaintenance();
} catch (ex) {
}
// Store our task list config data
this._tasks = tasksCfg;
// retrieve taskbar related prefs.
this._refreshPrefs();
// observer for private browsing and our prefs branch
this._initObs();
// jump list refresh timer
this._updateTimer();
},
update: function WTBJL_update() {
// are we disabled via prefs? don't do anything!
if (!this._enabled)
return;
// do what we came here to do, update the taskbar jumplist
this._buildList();
},
_shutdown: function WTBJL__shutdown() {
this._shuttingDown = true;
// Correctly handle a clear history on shutdown. If there are no
// entries be sure to empty all history lists. Luckily Places caches
// this value, so it's a pretty fast call.
if (!PlacesUtils.history.hasHistoryEntries) {
this.update();
}
this._free();
},
_shortcutMaintenance: function WTBJL__maintenace() {
_winShellService.shortcutMaintenance();
},
/**
* List building
*
* @note Async builders must add their mozIStoragePendingStatement to
* _pendingStatements object, using a different LIST_TYPE entry for
* each statement. Once finished they must remove it and call
* commitBuild(). When there will be no more _pendingStatements,
* commitBuild() will commit for real.
*/
_pendingStatements: {},
_hasPendingStatements: function WTBJL__hasPendingStatements() {
return Object.keys(this._pendingStatements).length > 0;
},
_buildList: function WTBJL__buildList() {
if (this._hasPendingStatements()) {
// We were requested to update the list while another update was in
// progress, this could happen at shutdown, idle or privatebrowsing.
// Abort the current list building.
for (let listType in this._pendingStatements) {
this._pendingStatements[listType].cancel();
delete this._pendingStatements[listType];
}
this._builder.abortListBuild();
}
// anything to build?
if (!this._showFrequent && !this._showRecent && !this._showTasks) {
// don't leave the last list hanging on the taskbar.
this._deleteActiveJumpList();
return;
}
if (!this._startBuild())
return;
if (this._showTasks)
this._buildTasks();
// Space for frequent items takes priority over recent.
if (this._showFrequent)
this._buildFrequent();
if (this._showRecent)
this._buildRecent();
this._commitBuild();
},
/**
* Taskbar api wrappers
*/
_startBuild: function WTBJL__startBuild() {
var removedItems = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
this._builder.abortListBuild();
if (this._builder.initListBuild(removedItems)) {
// Prior to building, delete removed items from history.
this._clearHistory(removedItems);
return true;
}
return false;
},
_commitBuild: function WTBJL__commitBuild() {
if (!this._hasPendingStatements() && !this._builder.commitListBuild()) {
this._builder.abortListBuild();
}
},
_buildTasks: function WTBJL__buildTasks() {
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
this._tasks.forEach(function (task) {
if ((this._shuttingDown && !task.close) || (!this._shuttingDown && !task.open))
return;
var item = this._getHandlerAppItem(task.title, task.description,
task.args, task.iconIndex, null);
items.appendElement(item, false);
}, this);
if (items.length > 0)
this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items);
},
_buildCustom: function WTBJL__buildCustom(title, items) {
if (items.length > 0)
this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title);
},
_buildFrequent: function WTBJL__buildFrequent() {
// If history is empty, just bail out.
if (!PlacesUtils.history.hasHistoryEntries) {
return;
}
// Windows supports default frequent and recent lists,
// but those depend on internal windows visit tracking
// which we don't populate. So we build our own custom
// frequent and recent lists using our nav history data.
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
// track frequent items so that we don't add them to
// the recent list.
this._frequentHashList = [];
this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults(
Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
this._maxItemCount,
function (aResult) {
if (!aResult) {
delete this._pendingStatements[LIST_TYPE.FREQUENT];
// The are no more results, build the list.
this._buildCustom(_getString("taskbar.frequent.label"), items);
this._commitBuild();
return;
}
let title = aResult.title || aResult.uri;
let faviconPageUri = Services.io.newURI(aResult.uri, null, null);
let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1,
faviconPageUri);
items.appendElement(shortcut, false);
this._frequentHashList.push(aResult.uri);
},
this
);
},
_buildRecent: function WTBJL__buildRecent() {
// If history is empty, just bail out.
if (!PlacesUtils.history.hasHistoryEntries) {
return;
}
var items = Cc["@mozilla.org/array;1"].
createInstance(Ci.nsIMutableArray);
// Frequent items will be skipped, so we select a double amount of
// entries and stop fetching results at _maxItemCount.
var count = 0;
this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults(
Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
this._maxItemCount * 2,
function (aResult) {
if (!aResult) {
// The are no more results, build the list.
this._buildCustom(_getString("taskbar.recent.label"), items);
delete this._pendingStatements[LIST_TYPE.RECENT];
this._commitBuild();
return;
}
if (count >= this._maxItemCount) {
return;
}
// Do not add items to recent that have already been added to frequent.
if (this._frequentHashList &&
this._frequentHashList.indexOf(aResult.uri) != -1) {
return;
}
let title = aResult.title || aResult.uri;
let faviconPageUri = Services.io.newURI(aResult.uri, null, null);
let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1,
faviconPageUri);
items.appendElement(shortcut, false);
count++;
},
this
);
},
_deleteActiveJumpList: function WTBJL__deleteAJL() {
this._builder.deleteActiveList();
},
/**
* Jump list item creation helpers
*/
_getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description,
args, iconIndex,
faviconPageUri) {
var file = Services.dirsvc.get("XREExeF", Ci.nsILocalFile);
var handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
createInstance(Ci.nsILocalHandlerApp);
handlerApp.executable = file;
// handlers default to the leaf name if a name is not specified
if (name && name.length != 0)
handlerApp.name = name;
handlerApp.detailedDescription = description;
handlerApp.appendParameter(args);
var item = Cc["@mozilla.org/windows-jumplistshortcut;1"].
createInstance(Ci.nsIJumpListShortcut);
item.app = handlerApp;
item.iconIndex = iconIndex;
item.faviconPageUri = faviconPageUri;
return item;
},
_getSeparatorItem: function WTBJL__getSeparatorItem() {
var item = Cc["@mozilla.org/windows-jumplistseparator;1"].
createInstance(Ci.nsIJumpListSeparator);
return item;
},
/**
* Nav history helpers
*/
_getHistoryResults:
function WTBLJL__getHistoryResults(aSortingMode, aLimit, aCallback, aScope) {
var options = PlacesUtils.history.getNewQueryOptions();
options.maxResults = aLimit;
options.sortingMode = aSortingMode;
var query = PlacesUtils.history.getNewQuery();
// Return the pending statement to the caller, to allow cancelation.
return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
.asyncExecuteLegacyQueries([query], 1, options, {
handleResult: function (aResultSet) {
for (let row; (row = aResultSet.getNextRow());) {
try {
aCallback.call(aScope,
{ uri: row.getResultByIndex(1)
, title: row.getResultByIndex(2)
});
} catch (e) {}
}
},
handleError: function (aError) {
Components.utils.reportError(
"Async execution error (" + aError.result + "): " + aError.message);
},
handleCompletion: function (aReason) {
aCallback.call(WinTaskbarJumpList, null);
},
});
},
_clearHistory: function WTBJL__clearHistory(items) {
if (!items)
return;
var URIsToRemove = [];
var e = items.enumerate();
while (e.hasMoreElements()) {
let oldItem = e.getNext().QueryInterface(Ci.nsIJumpListShortcut);
if (oldItem) {
try { // in case we get a bad uri
let uriSpec = oldItem.app.getParameter(0);
URIsToRemove.push(NetUtil.newURI(uriSpec));
} catch (err) { }
}
}
if (URIsToRemove.length > 0) {
PlacesUtils.bhistory.removePages(URIsToRemove, URIsToRemove.length, true);
}
},
/**
* Prefs utilities
*/
_refreshPrefs: function WTBJL__refreshPrefs() {
this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED);
this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT);
this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT);
this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS);
this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT);
},
/**
* Init and shutdown utilities
*/
_initTaskbar: function WTBJL__initTaskbar() {
this._builder = _taskbarService.createJumpListBuilder();
if (!this._builder || !this._builder.available)
return false;
return true;
},
_initObs: function WTBJL__initObs() {
// If the browser is closed while in private browsing mode, the "exit"
// notification is fired on quit-application-granted.
// History cleanup can happen at profile-change-teardown.
Services.obs.addObserver(this, "profile-before-change", false);
Services.obs.addObserver(this, "browser:purge-session-history", false);
_prefs.addObserver("", this, false);
},
_freeObs: function WTBJL__freeObs() {
Services.obs.removeObserver(this, "profile-before-change");
Services.obs.removeObserver(this, "browser:purge-session-history");
_prefs.removeObserver("", this);
},
_updateTimer: function WTBJL__updateTimer() {
if (this._enabled && !this._shuttingDown && !this._timer) {
this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this._timer.initWithCallback(this,
_prefs.getIntPref(PREF_TASKBAR_REFRESH)*1000,
this._timer.TYPE_REPEATING_SLACK);
}
else if ((!this._enabled || this._shuttingDown) && this._timer) {
this._timer.cancel();
delete this._timer;
}
},
_hasIdleObserver: false,
_updateIdleObserver: function WTBJL__updateIdleObserver() {
if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) {
_idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
this._hasIdleObserver = true;
}
else if ((!this._enabled || this._shuttingDown) && this._hasIdleObserver) {
_idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
this._hasIdleObserver = false;
}
},
_free: function WTBJL__free() {
this._freeObs();
this._updateTimer();
this._updateIdleObserver();
delete this._builder;
},
/**
* Notification handlers
*/
notify: function WTBJL_notify(aTimer) {
// Add idle observer on the first notification so it doesn't hit startup.
this._updateIdleObserver();
this.update();
},
observe: function WTBJL_observe(aSubject, aTopic, aData) {
switch (aTopic) {
case "nsPref:changed":
if (this._enabled == true && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED))
this._deleteActiveJumpList();
this._refreshPrefs();
this._updateTimer();
this._updateIdleObserver();
this.update();
break;
case "profile-before-change":
this._shutdown();
break;
case "browser:purge-session-history":
this.update();
break;
case "idle":
if (this._timer) {
this._timer.cancel();
delete this._timer;
}
break;
case "back":
this._updateTimer();
break;
}
},
};

View file

@ -0,0 +1,708 @@
/* vim: se cin sw=2 ts=2 et filetype=javascript :
* 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/. */
/*
* This module implements the front end behavior for AeroPeek. Starting in
* Windows Vista, the taskbar began showing live thumbnail previews of windows
* when the user hovered over the window icon in the taskbar. Starting with
* Windows 7, the taskbar allows an application to expose its tabbed interface
* in the taskbar by showing thumbnail previews rather than the default window
* preview. Additionally, when a user hovers over a thumbnail (tab or window),
* they are shown a live preview of the window (or tab + its containing window).
*
* In Windows 7, a title, icon, close button and optional toolbar are shown for
* each preview. This feature does not make use of the toolbar. For window
* previews, the title is the window title and the icon the window icon. For
* tab previews, the title is the page title and the page's favicon. In both
* cases, the close button "does the right thing."
*
* The primary objects behind this feature are nsITaskbarTabPreview and
* nsITaskbarPreviewController. Each preview has a controller. The controller
* responds to the user's interactions on the taskbar and provides the required
* data to the preview for determining the size of the tab and thumbnail. The
* PreviewController class implements this interface. The preview will request
* the controller to provide a thumbnail or preview when the user interacts with
* the taskbar. To reduce the overhead of drawing the tab area, the controller
* implementation caches the tab's contents in a <canvas> element. If no
* previews or thumbnails have been requested for some time, the controller will
* discard its cached tab contents.
*
* Screen real estate is limited so when there are too many thumbnails to fit
* on the screen, the taskbar stops displaying thumbnails and instead displays
* just the title, icon and close button in a similar fashion to previous
* versions of the taskbar. If there are still too many previews to fit on the
* screen, the taskbar resorts to a scroll up and scroll down button pair to let
* the user scroll through the list of tabs. Since this is undoubtedly
* inconvenient for users with many tabs, the AeroPeek objects turns off all of
* the tab previews. This tells the taskbar to revert to one preview per window.
* If the number of tabs falls below this magic threshold, the preview-per-tab
* behavior returns. There is no reliable way to determine when the scroll
* buttons appear on the taskbar, so a magic pref-controlled number determines
* when this threshold has been crossed.
*/
this.EXPORTED_SYMBOLS = ["AeroPeek"];
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
// Pref to enable/disable preview-per-tab
const TOGGLE_PREF_NAME = "browser.taskbar.previews.enable";
// Pref to determine the magic auto-disable threshold
const DISABLE_THRESHOLD_PREF_NAME = "browser.taskbar.previews.max";
// Pref to control the time in seconds that tab contents live in the cache
const CACHE_EXPIRATION_TIME_PREF_NAME = "browser.taskbar.previews.cachetime";
const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1";
////////////////////////////////////////////////////////////////////////////////
//// Various utility properties
XPCOMUtils.defineLazyServiceGetter(this, "ioSvc",
"@mozilla.org/network/io-service;1",
"nsIIOService");
XPCOMUtils.defineLazyServiceGetter(this, "imgTools",
"@mozilla.org/image/tools;1",
"imgITools");
XPCOMUtils.defineLazyServiceGetter(this, "faviconSvc",
"@mozilla.org/browser/favicon-service;1",
"nsIFaviconService");
// nsIURI -> imgIContainer
function _imageFromURI(uri, privateMode, callback) {
let channel = ioSvc.newChannelFromURI(uri);
try {
channel.QueryInterface(Ci.nsIPrivateBrowsingChannel);
channel.setPrivate(privateMode);
} catch (e) {
// Ignore channels which do not support nsIPrivateBrowsingChannel
}
NetUtil.asyncFetch(channel, function(inputStream, resultCode) {
if (!Components.isSuccessCode(resultCode))
return;
try {
let out_img = { value: null };
imgTools.decodeImageData(inputStream, channel.contentType, out_img);
callback(out_img.value);
} catch (e) {
// We failed, so use the default favicon (only if this wasn't the default
// favicon).
let defaultURI = faviconSvc.defaultFavicon;
if (!defaultURI.equals(uri))
_imageFromURI(defaultURI, callback);
}
});
}
// string? -> imgIContainer
function getFaviconAsImage(iconurl, privateMode, callback) {
if (iconurl)
_imageFromURI(NetUtil.newURI(iconurl), privateMode, callback);
else
_imageFromURI(faviconSvc.defaultFavicon, privateMode, callback);
}
// Snaps the given rectangle to be pixel-aligned at the given scale
function snapRectAtScale(r, scale) {
let x = Math.floor(r.x * scale);
let y = Math.floor(r.y * scale);
let width = Math.ceil((r.x + r.width) * scale) - x;
let height = Math.ceil((r.y + r.height) * scale) - y;
r.x = x / scale;
r.y = y / scale;
r.width = width / scale;
r.height = height / scale;
}
////////////////////////////////////////////////////////////////////////////////
//// PreviewController
/*
* This class manages the behavior of the preview.
*
* To give greater performance when drawing, the dirty areas of the content
* window are tracked and drawn on demand into a canvas of the same size.
* This provides a great increase in responsiveness when drawing a preview
* for unchanged (or even only slightly changed) tabs.
*
* @param win
* The TabWindow (see below) that owns the preview that this controls
* @param tab
* The <tab> that this preview is associated with
*/
function PreviewController(win, tab) {
this.win = win;
this.tab = tab;
this.linkedBrowser = tab.linkedBrowser;
this.preview = this.win.createTabPreview(this);
this.linkedBrowser.addEventListener("MozAfterPaint", this, false);
this.tab.addEventListener("TabAttrModified", this, false);
XPCOMUtils.defineLazyGetter(this, "canvasPreview", function () {
let canvas = this.win.win.document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
canvas.mozOpaque = true;
return canvas;
});
XPCOMUtils.defineLazyGetter(this, "dirtyRegion",
function () {
let dirtyRegion = Cc["@mozilla.org/gfx/region;1"]
.createInstance(Ci.nsIScriptableRegion);
dirtyRegion.init();
return dirtyRegion;
});
XPCOMUtils.defineLazyGetter(this, "winutils",
function () {
let win = tab.linkedBrowser.contentWindow;
return win.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
});
}
PreviewController.prototype = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsITaskbarPreviewController,
Ci.nsIDOMEventListener]),
destroy: function () {
this.tab.removeEventListener("TabAttrModified", this, false);
this.linkedBrowser.removeEventListener("MozAfterPaint", this, false);
// Break cycles, otherwise we end up leaking the window with everything
// attached to it.
delete this.win;
delete this.preview;
delete this.dirtyRegion;
},
get wrappedJSObject() {
return this;
},
get dirtyRects() {
let rectstream = this.dirtyRegion.getRects();
if (!rectstream)
return [];
let rects = [];
for (let i = 0; i < rectstream.length; i+= 4) {
let r = {x: rectstream[i],
y: rectstream[i+1],
width: rectstream[i+2],
height: rectstream[i+3]};
rects.push(r);
}
return rects;
},
// Resizes the canvasPreview to 0x0, essentially freeing its memory.
// updateCanvasPreview() will detect the size mismatch as a resize event
// the next time it is called.
resetCanvasPreview: function () {
this.canvasPreview.width = 0;
this.canvasPreview.height = 0;
},
get zoom() {
// Note that winutils.fullZoom accounts for "quantization" of the zoom factor
// from nsIMarkupDocumentViewer due to conversion through appUnits.
// We do -not- want screenPixelsPerCSSPixel here, because that would -also-
// incorporate any scaling that is applied due to hi-dpi resolution options.
return this.winutils.fullZoom;
},
// Updates the controller's canvas with the parts of the <browser> that need
// to be redrawn.
updateCanvasPreview: function () {
let win = this.linkedBrowser.contentWindow;
let bx = this.linkedBrowser.boxObject;
// Check for resize
if (bx.width != this.canvasPreview.width ||
bx.height != this.canvasPreview.height) {
// Invalidate the entire area and repaint
this.onTabPaint({left:0, top:0, right:win.innerWidth, bottom:win.innerHeight});
this.canvasPreview.width = bx.width;
this.canvasPreview.height = bx.height;
}
// Draw dirty regions
let ctx = this.canvasPreview.getContext("2d");
let scale = this.zoom;
let flags = this.canvasPreviewFlags;
// The dirty region may include parts that are offscreen so we clip to the
// canvas area.
this.dirtyRegion.intersectRect(0, 0, win.innerWidth, win.innerHeight);
this.dirtyRects.forEach(function (r) {
// We need to snap the rectangle to be pixel aligned in the destination
// coordinate space. Otherwise natively themed widgets might not draw.
snapRectAtScale(r, scale);
let x = r.x;
let y = r.y;
let width = r.width;
let height = r.height;
ctx.save();
ctx.scale(scale, scale);
ctx.translate(x, y);
ctx.drawWindow(win, x, y, width, height, "white", flags);
ctx.restore();
});
this.dirtyRegion.setToRect(0,0,0,0);
// If we're updating the canvas, then we're in the middle of a peek so
// don't discard the cache of previews.
AeroPeek.resetCacheTimer();
},
onTabPaint: function (rect) {
let x = Math.floor(rect.left),
y = Math.floor(rect.top),
width = Math.ceil(rect.right) - x,
height = Math.ceil(rect.bottom) - y;
this.dirtyRegion.unionRect(x, y, width, height);
},
updateTitleAndTooltip: function () {
let title = this.win.tabbrowser.getWindowTitleForBrowser(this.linkedBrowser);
this.preview.title = title;
this.preview.tooltip = title;
},
//////////////////////////////////////////////////////////////////////////////
//// nsITaskbarPreviewController
get width() {
return this.win.width;
},
get height() {
return this.win.height;
},
get thumbnailAspectRatio() {
let boxObject = this.tab.linkedBrowser.boxObject;
// Avoid returning 0
let tabWidth = boxObject.width || 1;
// Avoid divide by 0
let tabHeight = boxObject.height || 1;
return tabWidth / tabHeight;
},
drawPreview: function (ctx) {
let self = this;
this.win.tabbrowser.previewTab(this.tab, function () self.previewTabCallback(ctx));
// We must avoid having the frame drawn around the window. See bug 520807
return false;
},
previewTabCallback: function (ctx) {
// This will extract the resolution-scale component of the scaling we need,
// which should be applied to both chrome and content;
// the page zoom component is applied (to content only) within updateCanvasPreview.
let scale = this.winutils.screenPixelsPerCSSPixel / this.winutils.fullZoom;
ctx.save();
ctx.scale(scale, scale);
let width = this.win.width;
let height = this.win.height;
// Draw our toplevel window
ctx.drawWindow(this.win.win, 0, 0, width, height, "transparent");
// XXX (jfkthame): Pending tabs don't seem to draw with the proper scaling
// unless we use this block of code; but doing this for "normal" (loaded) tabs
// results in blurry rendering on hidpi systems, so we avoid it if possible.
// I don't understand why pending and loaded tabs behave differently here...
// (see bug 857061).
if (this.tab.hasAttribute("pending")) {
// Compositor, where art thou?
// Draw the tab content on top of the toplevel window
this.updateCanvasPreview();
let boxObject = this.linkedBrowser.boxObject;
ctx.translate(boxObject.x, boxObject.y);
ctx.drawImage(this.canvasPreview, 0, 0);
}
ctx.restore();
},
drawThumbnail: function (ctx, width, height) {
this.updateCanvasPreview();
let scale = width/this.linkedBrowser.boxObject.width;
ctx.scale(scale, scale);
ctx.drawImage(this.canvasPreview, 0, 0);
// Don't draw a frame around the thumbnail
return false;
},
onClose: function () {
this.win.tabbrowser.removeTab(this.tab);
},
onActivate: function () {
this.win.tabbrowser.selectedTab = this.tab;
// Accept activation - this will restore the browser window
// if it's minimized
return true;
},
//// nsIDOMEventListener
handleEvent: function (evt) {
switch (evt.type) {
case "MozAfterPaint":
if (evt.originalTarget === this.linkedBrowser.contentWindow) {
let clientRects = evt.clientRects;
let length = clientRects.length;
for (let i = 0; i < length; i++) {
let r = clientRects.item(i);
this.onTabPaint(r);
}
}
let preview = this.preview;
if (preview.visible)
preview.invalidate();
break;
case "TabAttrModified":
this.updateTitleAndTooltip();
break;
}
}
};
XPCOMUtils.defineLazyGetter(PreviewController.prototype, "canvasPreviewFlags",
function () { let canvasInterface = Ci.nsIDOMCanvasRenderingContext2D;
return canvasInterface.DRAWWINDOW_DRAW_VIEW
| canvasInterface.DRAWWINDOW_DRAW_CARET
| canvasInterface.DRAWWINDOW_ASYNC_DECODE_IMAGES
| canvasInterface.DRAWWINDOW_DO_NOT_FLUSH;
});
////////////////////////////////////////////////////////////////////////////////
//// TabWindow
/*
* This class monitors a browser window for changes to its tabs
*
* @param win
* The nsIDOMWindow browser window
*/
function TabWindow(win) {
this.win = win;
this.tabbrowser = win.gBrowser;
this.previews = new Map();
for (let i = 0; i < this.tabEvents.length; i++)
this.tabbrowser.tabContainer.addEventListener(this.tabEvents[i], this, false);
this.tabbrowser.addTabsProgressListener(this);
AeroPeek.windows.push(this);
let tabs = this.tabbrowser.tabs;
for (let i = 0; i < tabs.length; i++)
this.newTab(tabs[i]);
this.updateTabOrdering();
AeroPeek.checkPreviewCount();
}
TabWindow.prototype = {
_enabled: false,
tabEvents: ["TabOpen", "TabClose", "TabSelect", "TabMove"],
destroy: function () {
this._destroying = true;
let tabs = this.tabbrowser.tabs;
this.tabbrowser.removeTabsProgressListener(this);
for (let i = 0; i < this.tabEvents.length; i++)
this.tabbrowser.tabContainer.removeEventListener(this.tabEvents[i], this, false);
for (let i = 0; i < tabs.length; i++)
this.removeTab(tabs[i]);
let idx = AeroPeek.windows.indexOf(this.win.gTaskbarTabGroup);
AeroPeek.windows.splice(idx, 1);
AeroPeek.checkPreviewCount();
},
get width () {
return this.win.innerWidth;
},
get height () {
return this.win.innerHeight;
},
// Invoked when the given tab is added to this window
newTab: function (tab) {
let controller = new PreviewController(this, tab);
// It's OK to add the preview now while the favicon still loads.
this.previews.set(tab, controller.preview);
AeroPeek.addPreview(controller.preview);
// updateTitleAndTooltip relies on having controller.preview which is lazily resolved.
// Now that we've updated this.previews, it will resolve successfully.
controller.updateTitleAndTooltip();
},
createTabPreview: function (controller) {
let docShell = this.win
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
let preview = AeroPeek.taskbar.createTaskbarTabPreview(docShell, controller);
preview.visible = AeroPeek.enabled;
preview.active = this.tabbrowser.selectedTab == controller.tab;
// Grab the default favicon
getFaviconAsImage(null, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) {
// It is possible that we've already gotten the real favicon, so make sure
// we have not set one before setting this default one.
if (!preview.icon)
preview.icon = img;
});
return preview;
},
// Invoked when the given tab is closed
removeTab: function (tab) {
let preview = this.previewFromTab(tab);
preview.active = false;
preview.visible = false;
preview.move(null);
preview.controller.wrappedJSObject.destroy();
// We don't want to splice from the array if the tabs aren't being removed
// from the tab bar as well (as is the case when the window closes).
this.previews.delete(tab);
AeroPeek.removePreview(preview);
},
get enabled () {
return this._enabled;
},
set enabled (enable) {
this._enabled = enable;
// Because making a tab visible requires that the tab it is next to be
// visible, it is far simpler to unset the 'next' tab and recreate them all
// at once.
for (let [tab, preview] of this.previews) {
preview.move(null);
preview.visible = enable;
}
this.updateTabOrdering();
},
previewFromTab: function (tab) {
return this.previews.get(tab);
},
updateTabOrdering: function () {
let previews = this.previews;
let tabs = this.tabbrowser.tabs;
// Previews are internally stored using a map, so we need to iterate over
// the tabbrowser's array of tabs to retrieve previews in the same order.
let inorder = [previews.get(t) for (t of tabs) if (previews.has(t))];
// Since the internal taskbar array has not yet been updated, we must force
// the sorting order of our local array on it. To do so, we must walk
// the local array backwards, because otherwise we would send move requests
// in the wrong order. See bug 522610 for details.
for (let i = inorder.length - 1; i >= 0; i--) {
inorder[i].move(inorder[i + 1] || null);
}
},
//// nsIDOMEventListener
handleEvent: function (evt) {
let tab = evt.originalTarget;
switch (evt.type) {
case "TabOpen":
this.newTab(tab);
this.updateTabOrdering();
break;
case "TabClose":
this.removeTab(tab);
this.updateTabOrdering();
break;
case "TabSelect":
this.previewFromTab(tab).active = true;
break;
case "TabMove":
this.updateTabOrdering();
break;
}
},
//// Browser progress listener
onLinkIconAvailable: function (aBrowser, aIconURL) {
let self = this;
getFaviconAsImage(aIconURL, PrivateBrowsingUtils.isWindowPrivate(this.win), function (img) {
let index = self.tabbrowser.browsers.indexOf(aBrowser);
// Only add it if we've found the index. The tab could have closed!
if (index != -1) {
let tab = self.tabbrowser.tabs[index];
self.previews.get(tab).icon = img;
}
});
}
}
////////////////////////////////////////////////////////////////////////////////
//// AeroPeek
/*
* This object acts as global storage and external interface for this feature.
* It maintains the values of the prefs.
*/
this.AeroPeek = {
available: false,
// Does the pref say we're enabled?
_prefenabled: true,
_enabled: true,
// nsITaskbarTabPreview array
previews: [],
// TabWindow array
windows: [],
// nsIWinTaskbar service
taskbar: null,
// Maximum number of previews
maxpreviews: 20,
// Length of time in seconds that previews are cached
cacheLifespan: 20,
initialize: function () {
if (!(WINTASKBAR_CONTRACTID in Cc))
return;
this.taskbar = Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar);
this.available = this.taskbar.available;
if (!this.available)
return;
this.prefs.addObserver(TOGGLE_PREF_NAME, this, false);
this.prefs.addObserver(DISABLE_THRESHOLD_PREF_NAME, this, false);
this.prefs.addObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this, false);
this.cacheLifespan = this.prefs.getIntPref(CACHE_EXPIRATION_TIME_PREF_NAME);
this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
this.enabled = this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
},
destroy: function destroy() {
this._enabled = false;
this.prefs.removeObserver(TOGGLE_PREF_NAME, this);
this.prefs.removeObserver(DISABLE_THRESHOLD_PREF_NAME, this);
this.prefs.removeObserver(CACHE_EXPIRATION_TIME_PREF_NAME, this);
if (this.cacheTimer)
this.cacheTimer.cancel();
},
get enabled() {
return this._enabled;
},
set enabled(enable) {
if (this._enabled == enable)
return;
this._enabled = enable;
this.windows.forEach(function (win) {
win.enabled = enable;
});
},
addPreview: function (preview) {
this.previews.push(preview);
this.checkPreviewCount();
},
removePreview: function (preview) {
let idx = this.previews.indexOf(preview);
this.previews.splice(idx, 1);
this.checkPreviewCount();
},
checkPreviewCount: function () {
if (this.previews.length > this.maxpreviews)
this.enabled = false;
else
this.enabled = this._prefenabled;
},
onOpenWindow: function (win) {
// This occurs when the taskbar service is not available (xp, vista)
if (!this.available)
return;
win.gTaskbarTabGroup = new TabWindow(win);
},
onCloseWindow: function (win) {
// This occurs when the taskbar service is not available (xp, vista)
if (!this.available)
return;
win.gTaskbarTabGroup.destroy();
delete win.gTaskbarTabGroup;
if (this.windows.length == 0)
this.destroy();
},
resetCacheTimer: function () {
this.cacheTimer.cancel();
this.cacheTimer.init(this, 1000*this.cacheLifespan, Ci.nsITimer.TYPE_ONE_SHOT);
},
//// nsIObserver
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "nsPref:changed":
if (aData == CACHE_EXPIRATION_TIME_PREF_NAME)
break;
if (aData == TOGGLE_PREF_NAME)
this._prefenabled = this.prefs.getBoolPref(TOGGLE_PREF_NAME);
else if (aData == DISABLE_THRESHOLD_PREF_NAME)
this.maxpreviews = this.prefs.getIntPref(DISABLE_THRESHOLD_PREF_NAME);
// Might need to enable/disable ourselves
this.checkPreviewCount();
break;
case "timer-callback":
this.previews.forEach(function (preview) {
let controller = preview.controller.wrappedJSObject;
controller.resetCanvasPreview();
});
break;
}
}
};
XPCOMUtils.defineLazyGetter(AeroPeek, "cacheTimer", function ()
Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer)
);
XPCOMUtils.defineLazyServiceGetter(AeroPeek, "prefs",
"@mozilla.org/preferences-service;1",
"nsIPrefBranch");
AeroPeek.initialize();

View file

@ -0,0 +1,39 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
# XXX: Include this until we convert browser/ to use toolkit promises directly
EXTRA_JS_MODULES += [ 'promise.js' ]
EXTRA_JS_MODULES += [
'BrowserNewTabPreloader.jsm',
'CharsetMenu.jsm',
'FormSubmitObserver.jsm',
'FormValidationHandler.jsm',
'NetworkPrioritizer.jsm',
'offlineAppCache.jsm',
'openLocationLastURL.jsm',
'PageMenu.jsm',
'PopupNotifications.jsm',
'QuotaManager.jsm',
'SharedFrame.jsm',
'webrtcUI.jsm'
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
EXTRA_JS_MODULES += [
'Windows8WindowFrameColor.jsm',
'WindowsJumpLists.jsm',
'WindowsPreviewPerTab.jsm',
]
EXTRA_PP_JS_MODULES += [
'AboutHomeUtils.jsm',
'RecentWindow.jsm',
]
if CONFIG['MOZILLA_OFFICIAL']:
DEFINES['MOZILLA_OFFICIAL'] = 1

View file

@ -0,0 +1,20 @@
/* 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/. */
this.EXPORTED_SYMBOLS = ["OfflineAppCacheHelper"];
Components.utils.import('resource://gre/modules/LoadContextInfo.jsm');
const Cc = Components.classes;
const Ci = Components.interfaces;
this.OfflineAppCacheHelper = {
clear: function() {
var cacheService = Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService);
var appCacheStorage = cacheService.appCacheStorage(LoadContextInfo.default, null);
try {
appCacheStorage.asyncEvictStorage(null);
} catch(er) {}
}
};

View file

@ -0,0 +1,85 @@
/* 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/. */
const LAST_URL_PREF = "general.open_location.last_url";
const nsISupportsString = Components.interfaces.nsISupportsString;
const Ci = Components.interfaces;
Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
this.EXPORTED_SYMBOLS = [ "OpenLocationLastURL" ];
let prefSvc = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
let gOpenLocationLastURLData = "";
let observer = {
QueryInterface: function (aIID) {
if (aIID.equals(Components.interfaces.nsIObserver) ||
aIID.equals(Components.interfaces.nsISupports) ||
aIID.equals(Components.interfaces.nsISupportsWeakReference))
return this;
throw Components.results.NS_NOINTERFACE;
},
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "last-pb-context-exited":
gOpenLocationLastURLData = "";
break;
case "browser:purge-session-history":
prefSvc.clearUserPref(LAST_URL_PREF);
gOpenLocationLastURLData = "";
break;
}
}
};
let os = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
os.addObserver(observer, "last-pb-context-exited", true);
os.addObserver(observer, "browser:purge-session-history", true);
this.OpenLocationLastURL = function OpenLocationLastURL(aWindow) {
this.window = aWindow;
}
OpenLocationLastURL.prototype = {
isPrivate: function OpenLocationLastURL_isPrivate() {
// Assume not in private browsing mode, unless the browser window is
// in private mode.
if (!this.window)
return false;
return PrivateBrowsingUtils.isWindowPrivate(this.window);
},
get value() {
if (this.isPrivate())
return gOpenLocationLastURLData;
else {
try {
return prefSvc.getComplexValue(LAST_URL_PREF, nsISupportsString).data;
}
catch (e) {
return "";
}
}
},
set value(val) {
if (typeof val != "string")
val = "";
if (this.isPrivate())
gOpenLocationLastURLData = val;
else {
let str = Components.classes["@mozilla.org/supports-string;1"]
.createInstance(Components.interfaces.nsISupportsString);
str.data = val;
prefSvc.setComplexValue(LAST_URL_PREF, nsISupportsString, str);
}
},
reset: function() {
prefSvc.clearUserPref(LAST_URL_PREF);
gOpenLocationLastURLData = "";
}
};

View file

@ -0,0 +1,118 @@
/* 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/.
*/
'use strict';
/*
* Uses `Promise.jsm` as a core implementation, with additional sugar
* from previous implementation, with inspiration from `Q` and `when`
*
* https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm
* https://github.com/cujojs/when
* https://github.com/kriskowal/q
*/
const PROMISE_URI = 'resource://gre/modules/Promise.jsm';
getEnvironment.call(this, function ({ require, exports, module, Cu }) {
const Promise = Cu.import(PROMISE_URI, {}).Promise;
const { Debugging, defer, resolve, all, reject, race } = Promise;
module.metadata = {
'stability': 'unstable'
};
let promised = (function() {
// Note: Define shortcuts and utility functions here in order to avoid
// slower property accesses and unnecessary closure creations on each
// call of this popular function.
var call = Function.call;
var concat = Array.prototype.concat;
// Utility function that does following:
// execute([ f, self, args...]) => f.apply(self, args)
function execute (args) call.apply(call, args)
// Utility function that takes promise of `a` array and maybe promise `b`
// as arguments and returns promise for `a.concat(b)`.
function promisedConcat(promises, unknown) {
return promises.then(function (values) {
return resolve(unknown)
.then(function (value) values.concat([value]));
});
}
return function promised(f, prototype) {
/**
Returns a wrapped `f`, which when called returns a promise that resolves to
`f(...)` passing all the given arguments to it, which by the way may be
promises. Optionally second `prototype` argument may be provided to be used
a prototype for a returned promise.
## Example
var promise = promised(Array)(1, promise(2), promise(3))
promise.then(console.log) // => [ 1, 2, 3 ]
**/
return function promised(...args) {
// create array of [ f, this, args... ]
return [f, this, ...args].
// reduce it via `promisedConcat` to get promised array of fulfillments
reduce(promisedConcat, resolve([], prototype)).
// finally map that to promise of `f.apply(this, args...)`
then(execute);
};
};
})();
exports.promised = promised;
exports.all = all;
exports.defer = defer;
exports.resolve = resolve;
exports.reject = reject;
exports.race = race;
exports.Promise = Promise;
exports.Debugging = Debugging;
});
function getEnvironment (callback) {
let Cu, _exports, _module, _require;
// CommonJS / SDK
if (typeof(require) === 'function') {
Cu = require('chrome').Cu;
_exports = exports;
_module = module;
_require = require;
}
// JSM
else if (String(this).indexOf('BackstagePass') >= 0) {
Cu = this['Components'].utils;
_exports = this.Promise = {};
_module = { uri: __URI__, id: 'promise/core' };
_require = uri => {
let imports = {};
Cu.import(uri, imports);
return imports;
};
this.EXPORTED_SYMBOLS = ['Promise'];
// mozIJSSubScriptLoader.loadSubscript
} else if (~String(this).indexOf('Sandbox')) {
Cu = this['Components'].utils;
_exports = this;
_module = { id: 'promise/core' };
_require = uri => {};
}
callback({
Cu: Cu,
exports: _exports,
module: _module,
require: _require
});
}

View file

@ -0,0 +1,272 @@
/* 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/. */
"use strict";
this.EXPORTED_SYMBOLS = ["webrtcUI"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
"@mozilla.org/mediaManagerService;1",
"nsIMediaManagerService");
this.webrtcUI = {
init: function () {
Services.obs.addObserver(handleRequest, "getUserMedia:request", false);
Services.obs.addObserver(updateIndicators, "recording-device-events", false);
Services.obs.addObserver(removeBrowserSpecificIndicator, "recording-window-ended", false);
},
uninit: function () {
Services.obs.removeObserver(handleRequest, "getUserMedia:request");
Services.obs.removeObserver(updateIndicators, "recording-device-events");
Services.obs.removeObserver(removeBrowserSpecificIndicator, "recording-window-ended");
},
showGlobalIndicator: false,
get activeStreams() {
let contentWindowSupportsArray = MediaManagerService.activeMediaCaptureWindows;
let count = contentWindowSupportsArray.Count();
let activeStreams = [];
for (let i = 0; i < count; i++) {
let contentWindow = contentWindowSupportsArray.GetElementAt(i);
let browser = getBrowserForWindow(contentWindow);
let browserWindow = browser.ownerDocument.defaultView;
let tab = browserWindow.gBrowser &&
browserWindow.gBrowser._getTabForContentWindow(contentWindow.top);
activeStreams.push({
uri: contentWindow.location.href,
tab: tab,
browser: browser
});
}
return activeStreams;
}
}
function getBrowserForWindowId(aWindowID) {
return getBrowserForWindow(Services.wm.getOuterWindowWithId(aWindowID));
}
function getBrowserForWindow(aContentWindow) {
return aContentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
}
function handleRequest(aSubject, aTopic, aData) {
let {windowID: windowID, callID: callID} = JSON.parse(aData);
let params = aSubject.QueryInterface(Ci.nsIMediaStreamOptions);
Services.wm.getMostRecentWindow(null).navigator.mozGetUserMediaDevices(
function (devices) {
prompt(windowID, callID, params.audio, params.video || params.picture, devices);
},
function (error) {
// bug 827146 -- In the future, the UI should catch NO_DEVICES_FOUND
// and allow the user to plug in a device, instead of immediately failing.
denyRequest(callID, error);
}
);
}
function denyRequest(aCallID, aError) {
let msg = null;
if (aError) {
msg = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
msg.data = aError;
}
Services.obs.notifyObservers(msg, "getUserMedia:response:deny", aCallID);
}
function prompt(aWindowID, aCallID, aAudioRequested, aVideoRequested, aDevices) {
let audioDevices = [];
let videoDevices = [];
for (let device of aDevices) {
device = device.QueryInterface(Ci.nsIMediaDevice);
switch (device.type) {
case "audio":
if (aAudioRequested)
audioDevices.push(device);
break;
case "video":
if (aVideoRequested)
videoDevices.push(device);
break;
}
}
let requestType;
if (audioDevices.length && videoDevices.length)
requestType = "CameraAndMicrophone";
else if (audioDevices.length)
requestType = "Microphone";
else if (videoDevices.length)
requestType = "Camera";
else {
denyRequest(aCallID, "NO_DEVICES_FOUND");
return;
}
let contentWindow = Services.wm.getOuterWindowWithId(aWindowID);
let host = contentWindow.document.documentURIObject.host;
let browser = getBrowserForWindow(contentWindow);
let chromeDoc = browser.ownerDocument;
let chromeWin = chromeDoc.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
let message = stringBundle.getFormattedString("getUserMedia.share" + requestType + ".message",
[ host ]);
function listDevices(menupopup, devices) {
while (menupopup.lastChild)
menupopup.removeChild(menupopup.lastChild);
let deviceIndex = 0;
for (let device of devices) {
addDeviceToList(menupopup, device.name, deviceIndex);
deviceIndex++;
}
}
function addDeviceToList(menupopup, deviceName, deviceIndex) {
let menuitem = chromeDoc.createElement("menuitem");
menuitem.setAttribute("value", deviceIndex);
menuitem.setAttribute("label", deviceName);
menuitem.setAttribute("tooltiptext", deviceName);
menupopup.appendChild(menuitem);
}
chromeDoc.getElementById("webRTC-selectCamera").hidden = !videoDevices.length;
chromeDoc.getElementById("webRTC-selectMicrophone").hidden = !audioDevices.length;
let camMenupopup = chromeDoc.getElementById("webRTC-selectCamera-menupopup");
let micMenupopup = chromeDoc.getElementById("webRTC-selectMicrophone-menupopup");
listDevices(camMenupopup, videoDevices);
listDevices(micMenupopup, audioDevices);
if (requestType == "CameraAndMicrophone") {
addDeviceToList(camMenupopup, stringBundle.getString("getUserMedia.noVideo.label"), "-1");
addDeviceToList(micMenupopup, stringBundle.getString("getUserMedia.noAudio.label"), "-1");
}
let mainAction = {
label: PluralForm.get(requestType == "CameraAndMicrophone" ? 2 : 1,
stringBundle.getString("getUserMedia.shareSelectedDevices.label")),
accessKey: stringBundle.getString("getUserMedia.shareSelectedDevices.accesskey"),
callback: function () {
let allowedDevices = Cc["@mozilla.org/supports-array;1"]
.createInstance(Ci.nsISupportsArray);
if (videoDevices.length) {
let videoDeviceIndex = chromeDoc.getElementById("webRTC-selectCamera-menulist").value;
if (videoDeviceIndex != "-1")
allowedDevices.AppendElement(videoDevices[videoDeviceIndex]);
}
if (audioDevices.length) {
let audioDeviceIndex = chromeDoc.getElementById("webRTC-selectMicrophone-menulist").value;
if (audioDeviceIndex != "-1")
allowedDevices.AppendElement(audioDevices[audioDeviceIndex]);
}
if (allowedDevices.Count() == 0) {
denyRequest(aCallID);
return;
}
Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
}
};
let secondaryActions = [{
label: stringBundle.getString("getUserMedia.denyRequest.label"),
accessKey: stringBundle.getString("getUserMedia.denyRequest.accesskey"),
callback: function () {
denyRequest(aCallID);
}
}];
let options = null;
chromeWin.PopupNotifications.show(browser, "webRTC-shareDevices", message,
"webRTC-shareDevices-notification-icon", mainAction,
secondaryActions, options);
}
function updateIndicators() {
webrtcUI.showGlobalIndicator =
MediaManagerService.activeMediaCaptureWindows.Count() > 0;
let e = Services.wm.getEnumerator("navigator:browser");
while (e.hasMoreElements())
e.getNext().WebrtcIndicator.updateButton();
for (let {browser: browser} of webrtcUI.activeStreams)
showBrowserSpecificIndicator(browser);
}
function showBrowserSpecificIndicator(aBrowser) {
let hasVideo = {};
let hasAudio = {};
MediaManagerService.mediaCaptureWindowState(aBrowser.contentWindow,
hasVideo, hasAudio);
let captureState;
if (hasVideo.value && hasAudio.value) {
captureState = "CameraAndMicrophone";
} else if (hasVideo.value) {
captureState = "Camera";
} else if (hasAudio.value) {
captureState = "Microphone";
} else {
Cu.reportError("showBrowserSpecificIndicator: got neither video nor audio access");
return;
}
let chromeWin = aBrowser.ownerDocument.defaultView;
let stringBundle = chromeWin.gNavigatorBundle;
let message = stringBundle.getString("getUserMedia.sharing" + captureState + ".message2");
let windowId = aBrowser.contentWindow
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
let mainAction = {
label: "Continue Sharing", //stringBundle.getString("getUserMedia.continueSharing.label"),
accessKey: "C", //stringBundle.getString("getUserMedia.continueSharing.accesskey"),
callback: function () {},
dismiss: true
};
let secondaryActions = [{
label: "Stop Sharing", //stringBundle.getString("getUserMedia.stopSharing.label"),
accessKey: "S", //stringBundle.getString("getUserMedia.stopSharing.accesskey"),
callback: function () {
Services.obs.notifyObservers(null, "getUserMedia:revoke", windowId);
}
}];
let options = {
hideNotNow: true,
dismissed: true
};
chromeWin.PopupNotifications.show(aBrowser, "webRTC-sharingDevices", message,
"webRTC-sharingDevices-notification-icon", mainAction,
secondaryActions, options);
}
function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
let browser = getBrowserForWindowId(aData);
let PopupNotifications = browser.ownerDocument.defaultView.PopupNotifications;
let notification = PopupNotifications &&
PopupNotifications.getNotification("webRTC-sharingDevices",
browser);
if (notification)
PopupNotifications.remove(notification);
}