diff --git a/browser/components/translation/BingTranslator.jsm b/browser/components/translation/BingTranslator.jsm
index 3d016fb5706a..6442a1d8ff35 100644
--- a/browser/components/translation/BingTranslator.jsm
+++ b/browser/components/translation/BingTranslator.jsm
@@ -6,7 +6,7 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
-this.EXPORTED_SYMBOLS = [ "BingTranslation" ];
+this.EXPORTED_SYMBOLS = [ "BingTranslator" ];
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Log.jsm");
@@ -40,7 +40,7 @@ const MAX_REQUESTS = 15;
* @returns {Promise} A promise that will resolve when the translation
* task is finished.
*/
-this.BingTranslation = function(translationDocument, sourceLanguage, targetLanguage) {
+this.BingTranslator = function(translationDocument, sourceLanguage, targetLanguage) {
this.translationDocument = translationDocument;
this.sourceLanguage = sourceLanguage;
this.targetLanguage = targetLanguage;
@@ -50,7 +50,7 @@ this.BingTranslation = function(translationDocument, sourceLanguage, targetLangu
this._translatedCharacterCount = 0;
};
-this.BingTranslation.prototype = {
+this.BingTranslator.prototype = {
/**
* Performs the translation, splitting the document into several chunks
* respecting the data limits of the API.
@@ -282,7 +282,10 @@ BingRequest.prototype = {
return Task.spawn(function *(){
let token = yield BingTokenManager.getToken();
let auth = "Bearer " + token;
- let request = new RESTRequest("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray");
+ let url = getUrlParam("https://api.microsofttranslator.com/v2/Http.svc/TranslateArray",
+ "browser.translation.bing.translateArrayURL",
+ false);
+ let request = new RESTRequest(url);
request.setHeader("Content-type", "text/xml");
request.setHeader("Authorization", auth);
@@ -358,15 +361,18 @@ let BingTokenManager = {
* string once it is obtained.
*/
_getNewToken: function() {
- let request = new RESTRequest("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13");
+ let url = getUrlParam("https://datamarket.accesscontrol.windows.net/v2/OAuth2-13",
+ "browser.translation.bing.authURL",
+ false);
+ let request = new RESTRequest(url);
request.setHeader("Content-type", "application/x-www-form-urlencoded");
let params = [
"grant_type=client_credentials",
"scope=" + encodeURIComponent("http://api.microsofttranslator.com"),
"client_id=" +
- getAuthTokenParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
+ getUrlParam("%BING_API_CLIENTID%", "browser.translation.bing.clientIdOverride"),
"client_secret=" +
- getAuthTokenParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
+ getUrlParam("%BING_API_KEY%", "browser.translation.bing.apiKeyOverride")
];
let deferred = Promise.defer();
@@ -416,11 +422,10 @@ function escapeXML(aStr) {
* Fetch an auth token (clientID or client secret), which may be overridden by
* a pref if it's set.
*/
-function getAuthTokenParam(key, prefName) {
- let val;
- try {
- val = Services.prefs.getCharPref(prefName);
- } catch(ex) {}
+function getUrlParam(paramValue, prefName, encode = true) {
+ if (Services.prefs.getPrefType(prefName))
+ paramValue = Services.prefs.getCharPref(prefName);
+ paramValue = Services.urlFormatter.formatURL(paramValue);
- return encodeURIComponent(Services.urlFormatter.formatURL(val || key));
+ return encode ? encodeURIComponent(paramValue) : paramValue;
}
diff --git a/browser/components/translation/TranslationContentHandler.jsm b/browser/components/translation/TranslationContentHandler.jsm
index 41c04ed25941..773cfcf253c3 100644
--- a/browser/components/translation/TranslationContentHandler.jsm
+++ b/browser/components/translation/TranslationContentHandler.jsm
@@ -121,16 +121,16 @@ TranslationContentHandler.prototype = {
// translated text.
let translationDocument = this.global.content.translationDocument ||
new TranslationDocument(this.global.content.document);
- let bingTranslation = new BingTranslation(translationDocument,
- msg.data.from,
- msg.data.to);
+ let bingTranslator = new BingTranslator(translationDocument,
+ msg.data.from,
+ msg.data.to);
this.global.content.translationDocument = translationDocument;
translationDocument.translatedFrom = msg.data.from;
translationDocument.translatedTo = msg.data.to;
translationDocument.translationError = false;
- bingTranslation.translate().then(
+ bingTranslator.translate().then(
result => {
this.global.sendAsyncMessage("Translation:Finished", {
characterCount: result.characterCount,
diff --git a/browser/components/translation/test/bing.sjs b/browser/components/translation/test/bing.sjs
new file mode 100644
index 000000000000..81bd6732b2c3
--- /dev/null
+++ b/browser/components/translation/test/bing.sjs
@@ -0,0 +1,218 @@
+/* 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, Constructor: CC} = Components;
+const BinaryInputStream = CC("@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream");
+
+function handleRequest(req, res) {
+ try {
+ reallyHandleRequest(req, res);
+ } catch (ex) {
+ res.setStatusLine("1.0", 200, "AlmostOK");
+ let msg = "Error handling request: " + ex + "\n" + ex.stack;
+ log(msg);
+ res.write(msg);
+ }
+}
+
+function log(msg) {
+ // dump("BING-SERVER-MOCK: " + msg + "\n");
+}
+
+const statusCodes = {
+ 400: "Bad Request",
+ 401: "Unauthorized",
+ 403: "Forbidden",
+ 404: "Not Found",
+ 405: "Method Not Allowed",
+ 500: "Internal Server Error",
+ 501: "Not Implemented",
+ 503: "Service Unavailable"
+};
+
+function HTTPError(code = 500, message) {
+ this.code = code;
+ this.name = statusCodes[code] || "HTTPError";
+ this.message = message || this.name;
+}
+HTTPError.prototype = new Error();
+HTTPError.prototype.constructor = HTTPError;
+
+function sendError(res, err) {
+ if (!(err instanceof HTTPError)) {
+ err = new HTTPError(typeof err == "number" ? err : 500,
+ err.message || typeof err == "string" ? err : "");
+ }
+ res.setStatusLine("1.1", err.code, err.name);
+ res.write(err.message);
+}
+
+function parseQuery(query) {
+ let ret = {};
+ for (let param of query.replace(/^[?&]/, "").split("&")) {
+ param = param.split("=");
+ if (!param[0])
+ continue;
+ ret[unescape(param[0])] = unescape(param[1]);
+ }
+ return ret;
+}
+
+function getRequestBody(req) {
+ let avail;
+ let bytes = [];
+ let body = new BinaryInputStream(req.bodyInputStream);
+
+ while ((avail = body.available()) > 0)
+ Array.prototype.push.apply(bytes, body.readByteArray(avail));
+
+ return String.fromCharCode.apply(null, bytes);
+}
+
+function sha1(str) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+ .createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ // `result` is an out parameter, `result.value` will contain the array length.
+ let result = {};
+ // `data` is an array of bytes.
+ let data = converter.convertToByteArray(str, result);
+ let ch = Cc["@mozilla.org/security/hash;1"]
+ .createInstance(Ci.nsICryptoHash);
+ ch.init(ch.SHA1);
+ ch.update(data, data.length);
+ let hash = ch.finish(false);
+
+ // Return the two-digit hexadecimal code for a byte.
+ function toHexString(charCode) {
+ return ("0" + charCode.toString(16)).slice(-2);
+ }
+
+ // Convert the binary hash data to a hex string.
+ return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+}
+
+function parseXml(body) {
+ let DOMParser = Cc["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Ci.nsIDOMParser);
+ let xml = DOMParser.parseFromString(body, "text/xml");
+ if (xml.documentElement.localName == "parsererror")
+ throw new Error("Invalid XML");
+ return xml;
+}
+
+function getInputStream(path) {
+ let file = Cc["@mozilla.org/file/directory_service;1"]
+ .getService(Ci.nsIProperties)
+ .get("CurWorkD", Ci.nsILocalFile);
+ for (let part of path.split("/"))
+ file.append(part);
+ let fileStream = Cc["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Ci.nsIFileInputStream);
+ fileStream.init(file, 1, 0, false);
+ return fileStream;
+}
+
+function checkAuth(req) {
+ let err = new Error("Authorization failed");
+ err.code = 401;
+
+ if (!req.hasHeader("Authorization"))
+ throw new HTTPError(401, "No Authorization header provided.");
+
+ let auth = req.getHeader("Authorization");
+ if (!auth.startsWith("Bearer "))
+ throw new HTTPError(401, "Invalid Authorization header content: '" + auth + "'");
+}
+
+function reallyHandleRequest(req, res) {
+ log("method: " + req.method);
+ if (req.method != "POST") {
+ sendError(res, "Bing only deals with POST requests, not '" + req.method + "'.");
+ return;
+ }
+
+ let body = getRequestBody(req);
+ log("body: " + body);
+
+ // First, we'll see if we're dealing with an XML body:
+ let contentType = req.hasHeader("Content-Type") ? req.getHeader("Content-Type") : null;
+ log("contentType: " + contentType);
+
+ if (contentType == "text/xml") {
+ try {
+ // For all these requests the client needs to supply the correct
+ // authentication headers.
+ checkAuth(req);
+
+ let xml = parseXml(body);
+ let method = xml.documentElement.localName;
+ log("invoking method: " + method);
+ // If the requested method is supported, delegate it to its handler.
+ if (methodHandlers[method])
+ methodHandlers[method](res, xml);
+ else
+ throw new HTTPError(501);
+ } catch (ex) {
+ sendError(res, ex, ex.code);
+ }
+ } else {
+ // Not XML, so it must be a query-string.
+ let params = parseQuery(body);
+
+ // Delegate an authentication request to the correct handler.
+ if ("grant_type" in params && params.grant_type == "client_credentials")
+ methodHandlers.authenticate(res, params);
+ else
+ sendError(res, 501);
+ }
+}
+
+const methodHandlers = {
+ authenticate: function(res, params) {
+ // Validate a few required parameters.
+ if (params.scope != "http://api.microsofttranslator.com") {
+ sendError(res, "Invalid scope.");
+ return;
+ }
+ if (!params.client_id) {
+ sendError(res, "Missing client_id param.");
+ return;
+ }
+ if (!params.client_secret) {
+ sendError(res, "Missing client_secret param.");
+ return;
+ }
+
+ let content = JSON.stringify({
+ access_token: "test",
+ expires_in: 600
+ });
+
+ res.setStatusLine("1.1", 200, "OK");
+ res.setHeader("Content-Length", String(content.length));
+ res.setHeader("Content-Type", "application/json");
+ res.write(content);
+ },
+
+ TranslateArrayRequest: function(res, xml, body) {
+ let from = xml.querySelector("From").firstChild.nodeValue;
+ let to = xml.querySelector("To").firstChild.nodeValue
+ log("translating from '" + from + "' to '" + to + "'");
+
+ res.setStatusLine("1.1", 200, "OK");
+ res.setHeader("Content-Type", "text/xml");
+
+ let hash = sha1(body).substr(0, 10);
+ log("SHA1 hash of content: " + hash);
+ let inputStream = getInputStream(
+ "browser/browser/components/translation/test/fixtures/result-" + hash + ".txt");
+ res.bodyOutputStream.writeFrom(inputStream, inputStream.available());
+ inputStream.close();
+ }
+};
diff --git a/browser/components/translation/test/browser.ini b/browser/components/translation/test/browser.ini
index 8760691c5b72..64052248db26 100644
--- a/browser/components/translation/test/browser.ini
+++ b/browser/components/translation/test/browser.ini
@@ -1,6 +1,10 @@
[DEFAULT]
+support-files =
+ bing.sjs
+ fixtures/bug1022725-fr.html
+ fixtures/result-da39a3ee5e.txt
+[browser_translation_bing.js]
[browser_translation_fhr.js]
-skip-if = true # Needs to wait until bug 1022725.
[browser_translation_infobar.js]
[browser_translation_exceptions.js]
diff --git a/browser/components/translation/test/browser_translation_bing.js b/browser/components/translation/test/browser_translation_bing.js
new file mode 100644
index 000000000000..7f9251df8d30
--- /dev/null
+++ b/browser/components/translation/test/browser_translation_bing.js
@@ -0,0 +1,56 @@
+/* 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/. */
+
+// Test the Bing Translator client against a mock Bing service, bing.sjs.
+
+"use strict";
+
+const kClientIdPref = "browser.translation.bing.clientIdOverride";
+const kClientSecretPref = "browser.translation.bing.apiKeyOverride";
+
+const {BingTranslator} = Cu.import("resource:///modules/translation/BingTranslator.jsm", {});
+const {TranslationDocument} = Cu.import("resource:///modules/translation/TranslationDocument.jsm", {});
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setCharPref(kClientIdPref, "testClient");
+ Services.prefs.setCharPref(kClientSecretPref, "testSecret");
+
+ // Deduce the Mochitest server address in use from a pref that was pre-processed.
+ let server = Services.prefs.getCharPref("browser.translation.bing.authURL")
+ .replace("http://", "");
+ server = server.substr(0, server.indexOf("/"));
+ let tab = gBrowser.addTab("http://" + server +
+ "/browser/browser/components/translation/test/fixtures/bug1022725-fr.html");
+ gBrowser.selectedTab = tab;
+
+ registerCleanupFunction(function () {
+ gBrowser.removeTab(tab);
+ Services.prefs.clearUserPref(kClientIdPref);
+ Services.prefs.clearUserPref(kClientSecretPref);
+ });
+
+ let browser = tab.linkedBrowser;
+ browser.addEventListener("load", function onload() {
+ if (browser.currentURI.spec == "about:blank")
+ return;
+
+ browser.removeEventListener("load", onload, true);
+ let client = new BingTranslator(
+ new TranslationDocument(browser.contentDocument), "fr", "en");
+
+ client.translate().then(
+ result => {
+ // XXXmikedeboer; here you would continue the test/ content inspection.
+ ok(result, "There should be a result.");
+ finish();
+ },
+ error => {
+ ok(false, "Unexpected Client Error: " + error);
+ finish();
+ }
+ );
+ }, true);
+}
diff --git a/browser/components/translation/test/browser_translation_fhr.js b/browser/components/translation/test/browser_translation_fhr.js
index 5ab4c1a6df20..cf14c8e6dd72 100644
--- a/browser/components/translation/test/browser_translation_fhr.js
+++ b/browser/components/translation/test/browser_translation_fhr.js
@@ -57,7 +57,8 @@ function retrieveTranslationCounts() {
return [0, 0];
}
- return [day.get("pageTranslatedCount"), day.get("charactersTranslatedCount")];
+ // .get() may return `undefined`, which we can't compute.
+ return [day.get("pageTranslatedCount") || 0, day.get("charactersTranslatedCount") || 0];
});
}
diff --git a/browser/components/translation/test/fixtures/bug1022725-fr.html b/browser/components/translation/test/fixtures/bug1022725-fr.html
new file mode 100644
index 000000000000..f30edf52eb20
--- /dev/null
+++ b/browser/components/translation/test/fixtures/bug1022725-fr.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+ test
+
+
+ Coupe du monde de football de 2014
+ La Coupe du monde de football de 2014 est la 20e édition de la Coupe du monde de football, compétition organisée par la FIFA et qui réunit les trente-deux meilleures sélections nationales. Sa phase finale a lieu à l'été 2014 au Brésil. Avec le pays organisateur, toutes les équipes championnes du monde depuis 1930 (Uruguay, Italie, Allemagne, Angleterre, Argentine, France et Espagne) se sont qualifiées pour cette compétition. Elle est aussi la première compétition internationale de la Bosnie-Herzégovine.
+
+
diff --git a/browser/components/translation/test/fixtures/result-da39a3ee5e.txt b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt
new file mode 100644
index 000000000000..d2d14c78852c
--- /dev/null
+++ b/browser/components/translation/test/fixtures/result-da39a3ee5e.txt
@@ -0,0 +1,22 @@
+
+
+ fr
+
+ 34
+
+ Football's 2014 World Cup
+
+ 25
+
+
+
+ fr
+
+ 508
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus diam sem, porttitor eget neque sit amet, ultricies posuere metus. Cras placerat rutrum risus, nec dignissim magna dictum vitae. Fusce eleifend fermentum lacinia. Nulla sagittis cursus nibh. Praesent adipiscing, elit at pulvinar dapibus, neque massa tincidunt sapien, eu consectetur lectus metus sit amet odio. Proin blandit consequat porttitor. Pellentesque vehicula justo sed luctus vestibulum. Donec metus.
+
+ 475
+
+
+
diff --git a/browser/components/translation/translation-infobar.xml b/browser/components/translation/translation-infobar.xml
index c63a4a262535..585bc3224401 100644
--- a/browser/components/translation/translation-infobar.xml
+++ b/browser/components/translation/translation-infobar.xml
@@ -152,11 +152,6 @@
-
+
diff --git a/browser/devtools/app-manager/simulators-store.js b/browser/devtools/app-manager/simulators-store.js
index d6626d0530c5..6fc757330b95 100644
--- a/browser/devtools/app-manager/simulators-store.js
+++ b/browser/devtools/app-manager/simulators-store.js
@@ -10,7 +10,11 @@ let store = new ObservableObject({versions:[]});
function feedStore() {
store.object.versions = Simulator.availableVersions().map(v => {
- return {version:v}
+ let simulator = Simulator.getByVersion(v);
+ return {
+ version: v,
+ label: simulator.appinfo.label
+ }
});
}
diff --git a/browser/devtools/webide/content/addons.js b/browser/devtools/webide/content/addons.js
new file mode 100644
index 000000000000..7da12b3cf2fd
--- /dev/null
+++ b/browser/devtools/webide/content/addons.js
@@ -0,0 +1,117 @@
+/* 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 Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {GetAvailableAddons} = require("devtools/webide/addons");
+const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#aboutaddons").onclick = function() {
+ window.parent.UI.openInBrowser("about:addons");
+ }
+ document.querySelector("#close").onclick = CloseUI;
+ GetAvailableAddons().then(BuildUI, (e) => {
+ console.error(e);
+ window.alert(Strings.formatStringFromName("error_cantFetchAddonsJSON", [e], 1));
+ });
+}, true);
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function BuildUI(addons) {
+ BuildItem(addons.adb, true /* is adb */);
+ for (let addon of addons.simulators) {
+ BuildItem(addon, false /* is adb */);
+ }
+}
+
+function BuildItem(addon, isADB) {
+
+ function onAddonUpdate(event, arg) {
+ switch (event) {
+ case "update":
+ progress.removeAttribute("value");
+ li.setAttribute("status", addon.status);
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ break;
+ case "failure":
+ console.error(arg);
+ window.alert(arg);
+ break;
+ case "progress":
+ if (arg == -1) {
+ progress.removeAttribute("value");
+ } else {
+ progress.value = arg;
+ }
+ break;
+ }
+ }
+
+ let events = ["update", "failure", "progress"];
+ for (let e of events) {
+ addon.on(e, onAddonUpdate);
+ }
+ window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ for (let e of events) {
+ addon.off(e, onAddonUpdate);
+ }
+ });
+
+ let li = document.createElement("li");
+ li.setAttribute("status", addon.status);
+
+ // Used in tests
+ if (isADB) {
+ li.setAttribute("addon", "adb");
+ } else {
+ li.setAttribute("addon", "simulator-" + addon.version);
+ }
+
+ let name = document.createElement("span");
+ name.className = "name";
+ if (isADB) {
+ name.textContent = Strings.GetStringFromName("addons_adb_label");
+ } else {
+ let stability = Strings.GetStringFromName("addons_" + addon.stability);
+ name.textContent = Strings.formatStringFromName("addons_simulator_label", [addon.version, stability], 2);
+ }
+
+ li.appendChild(name);
+
+ let status = document.createElement("span");
+ status.className = "status";
+ status.textContent = Strings.GetStringFromName("addons_status_" + addon.status);
+ li.appendChild(status);
+
+ let installButton = document.createElement("button");
+ installButton.className = "install-button";
+ installButton.onclick = () => addon.install();
+ installButton.textContent = Strings.GetStringFromName("addons_install_button");
+ li.appendChild(installButton);
+
+ let uninstallButton = document.createElement("button");
+ uninstallButton.className = "uninstall-button";
+ uninstallButton.onclick = () => addon.uninstall();
+ uninstallButton.textContent = Strings.GetStringFromName("addons_uninstall_button");
+ li.appendChild(uninstallButton);
+
+ let progress = document.createElement("progress");
+ li.appendChild(progress);
+
+ if (isADB) {
+ let warning = document.createElement("p");
+ warning.textContent = Strings.GetStringFromName("addons_adb_warning");
+ warning.className = "warning";
+ li.appendChild(warning);
+ }
+
+ document.querySelector("ul").appendChild(li);
+}
diff --git a/browser/devtools/webide/content/addons.xhtml b/browser/devtools/webide/content/addons.xhtml
new file mode 100644
index 000000000000..12ed00988c0d
--- /dev/null
+++ b/browser/devtools/webide/content/addons.xhtml
@@ -0,0 +1,30 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+ &addons_title;
+
+
+
+
+
diff --git a/browser/devtools/webide/content/jar.mn b/browser/devtools/webide/content/jar.mn
index dbc606018422..1e17d4f3df9d 100644
--- a/browser/devtools/webide/content/jar.mn
+++ b/browser/devtools/webide/content/jar.mn
@@ -11,6 +11,12 @@ webide.jar:
content/details.xhtml (details.xhtml)
content/details.js (details.js)
content/cli.js (cli.js)
+ content/addons.js (addons.js)
+ content/addons.xhtml (addons.xhtml)
+ content/permissionstable.js (permissionstable.js)
+ content/permissionstable.xhtml (permissionstable.xhtml)
+ content/runtimedetails.js (runtimedetails.js)
+ content/runtimedetails.xhtml (runtimedetails.xhtml)
# Temporarily include locales in content, until we're ready
# to localize webide
diff --git a/browser/devtools/webide/content/newapp.js b/browser/devtools/webide/content/newapp.js
index e6451b4ee985..83f5ea18a0f6 100644
--- a/browser/devtools/webide/content/newapp.js
+++ b/browser/devtools/webide/content/newapp.js
@@ -17,6 +17,7 @@ const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
const {AppProjects} = require("devtools/app-manager/app-projects");
const APP_CREATOR_LIST = "devtools.webide.templatesURL";
const {AppManager} = require("devtools/webide/app-manager");
+const {GetTemplatesJSON} = require("devtools/webide/remote-resources");
let gTemplateList = null;
@@ -33,20 +34,12 @@ window.addEventListener("load", function onLoad() {
}, true);
function getJSON() {
- let xhr = new XMLHttpRequest();
- xhr.overrideMimeType('text/plain');
- xhr.onload = function() {
- let list;
- try {
- list = JSON.parse(this.responseText);
- if (!Array.isArray(list)) {
- throw new Error("JSON response not an array");
- }
- if (list.length == 0) {
- throw new Error("JSON response is an empty array");
- }
- } catch(e) {
- return failAndBail("Invalid response from server");
+ GetTemplatesJSON().then(list => {
+ if (!Array.isArray(list)) {
+ throw new Error("JSON response not an array");
+ }
+ if (list.length == 0) {
+ throw new Error("JSON response is an empty array");
}
gTemplateList = list;
let templatelistNode = document.querySelector("#templatelist");
@@ -76,13 +69,9 @@ function getJSON() {
document.querySelector("#project-name").value = testOptions.name;
doOK();
}
- };
- xhr.onerror = function() {
- failAndBail("Can't download app templates");
- };
- let url = Services.prefs.getCharPref(APP_CREATOR_LIST);
- xhr.open("get", url);
- xhr.send();
+ }, (e) => {
+ failAndBail("Can't download app templates: " + e);
+ });
}
function failAndBail(msg) {
diff --git a/browser/devtools/webide/content/permissionstable.js b/browser/devtools/webide/content/permissionstable.js
new file mode 100644
index 000000000000..6dc356bfeeb6
--- /dev/null
+++ b/browser/devtools/webide/content/permissionstable.js
@@ -0,0 +1,75 @@
+/* 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 Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {AppManager} = require("devtools/webide/app-manager");
+const {Connection} = require("devtools/client/connection-manager");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#close").onclick = CloseUI;
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "list-tabs-response") {
+ BuildUI();
+ }
+}
+
+let getRawPermissionsTablePromise; // Used by tests
+function BuildUI() {
+ let table = document.querySelector("table");
+ let lines = table.querySelectorAll(".line");
+ for (let line of lines) {
+ line.remove();
+ }
+
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.deviceFront) {
+ getRawPermissionsTablePromise = AppManager.deviceFront.getRawPermissionsTable();
+ getRawPermissionsTablePromise.then(json => {
+ let permissionsTable = json.rawPermissionsTable;
+ for (let name in permissionsTable) {
+ let tr = document.createElement("tr");
+ tr.className = "line";
+ let td = document.createElement("td");
+ td.textContent = name;
+ tr.appendChild(td);
+ for (let type of ["app","privileged","certified"]) {
+ let td = document.createElement("td");
+ if (permissionsTable[name][type] == json.ALLOW_ACTION) {
+ td.textContent = "✓";
+ td.className = "permallow";
+ }
+ if (permissionsTable[name][type] == json.PROMPT_ACTION) {
+ td.textContent = "!";
+ td.className = "permprompt";
+ }
+ if (permissionsTable[name][type] == json.DENY_ACTION) {
+ td.textContent = "✕";
+ td.className = "permdeny"
+ }
+ tr.appendChild(td);
+ }
+ table.appendChild(tr);
+ }
+ });
+ } else {
+ CloseUI();
+ }
+}
diff --git a/browser/devtools/webide/content/permissionstable.xhtml b/browser/devtools/webide/content/permissionstable.xhtml
new file mode 100644
index 000000000000..17dffd1b9e36
--- /dev/null
+++ b/browser/devtools/webide/content/permissionstable.xhtml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+ &permissionstable_title;
+
+
+
+ &permissionstable_name_header;
+ type:web
+ type:privileged
+ type:certified
+
+
+
+
diff --git a/browser/devtools/webide/content/runtimedetails.js b/browser/devtools/webide/content/runtimedetails.js
new file mode 100644
index 000000000000..8de2196dada2
--- /dev/null
+++ b/browser/devtools/webide/content/runtimedetails.js
@@ -0,0 +1,56 @@
+/* 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 Cu = Components.utils;
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const {AppManager} = require("devtools/webide/app-manager");
+const {Connection} = require("devtools/client/connection-manager");
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ document.querySelector("#close").onclick = CloseUI;
+ AppManager.on("app-manager-update", OnAppManagerUpdate);
+ BuildUI();
+}, true);
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ AppManager.off("app-manager-update", OnAppManagerUpdate);
+});
+
+function CloseUI() {
+ window.parent.UI.openProject();
+}
+
+function OnAppManagerUpdate(event, what) {
+ if (what == "connection" || what == "list-tabs-response") {
+ BuildUI();
+ }
+}
+
+let getDescriptionPromise; // Used by tests
+function BuildUI() {
+ let table = document.querySelector("table");
+ table.innerHTML = "";
+ if (AppManager.connection &&
+ AppManager.connection.status == Connection.Status.CONNECTED &&
+ AppManager.deviceFront) {
+ getDescriptionPromise = AppManager.deviceFront.getDescription();
+ getDescriptionPromise.then(json => {
+ for (let name in json) {
+ let tr = document.createElement("tr");
+ let td = document.createElement("td");
+ td.textContent = name;
+ tr.appendChild(td);
+ td = document.createElement("td");
+ td.textContent = json[name];
+ tr.appendChild(td);
+ table.appendChild(tr);
+ }
+ });
+ } else {
+ CloseUI();
+ }
+}
diff --git a/browser/devtools/webide/content/runtimedetails.xhtml b/browser/devtools/webide/content/runtimedetails.xhtml
new file mode 100644
index 000000000000..6d1f5d6dec1f
--- /dev/null
+++ b/browser/devtools/webide/content/runtimedetails.xhtml
@@ -0,0 +1,28 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+ &runtimedetails_title;
+
+
+
+
diff --git a/browser/devtools/webide/content/webide.js b/browser/devtools/webide/content/webide.js
index 1e8d6ec12814..631247872805 100644
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -18,12 +18,19 @@ const {Connection} = require("devtools/client/connection-manager");
const {AppManager} = require("devtools/webide/app-manager");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const ProjectEditor = require("projecteditor/projecteditor");
+const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
+const {GetAvailableAddons} = require("devtools/webide/addons");
+const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
const Strings = Services.strings.createBundle("chrome://webide/content/webide.properties");
const HTML = "http://www.w3.org/1999/xhtml";
const HELP_URL = "https://developer.mozilla.org/Firefox_OS/Using_the_App_Manager#Troubleshooting";
+// download some JSON early.
+GetTemplatesJSON(true);
+GetAddonsJSON(true);
+
// See bug 989619
console.log = console.log.bind(console);
console.warn = console.warn.bind(console);
@@ -56,18 +63,32 @@ let UI = {
window.addEventListener("focus", this.onfocus, true);
AppProjects.load().then(() => {
- let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation");
- if (lastProjectLocation) {
- let lastProject = AppProjects.get(lastProjectLocation);
- if (lastProject) {
- AppManager.selectedProject = lastProject;
- } else {
- AppManager.selectedProject = null;
- }
+ this.openLastProject();
+ });
+
+ // Auto install the ADB Addon Helper. Only once.
+ // If the user decides to uninstall the addon, we won't install it again.
+ let autoInstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
+ if (autoInstallADBHelper && !Devices.helperAddonInstalled) {
+ GetAvailableAddons().then(addons => {
+ addons.adb.install();
+ }, console.error);
+ }
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+ },
+
+ openLastProject: function() {
+ let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation");
+ if (lastProjectLocation) {
+ let lastProject = AppProjects.get(lastProjectLocation);
+ if (lastProject) {
+ AppManager.selectedProject = lastProject;
} else {
AppManager.selectedProject = null;
}
- });
+ } else {
+ AppManager.selectedProject = null;
+ }
},
uninit: function() {
@@ -203,7 +224,7 @@ let UI = {
}
}];
- let nbox = document.querySelector("#body");
+ let nbox = document.querySelector("#notificationbox");
nbox.removeAllNotifications(true);
nbox.appendNotification(text, "webide:errornotification", null,
nbox.PRIORITY_WARNING_LOW, buttons);
@@ -216,6 +237,28 @@ let UI = {
let simulatorListNode = document.querySelector("#runtime-panel-simulators");
let customListNode = document.querySelector("#runtime-panel-custom");
+ let noHelperNode = document.querySelector("#runtime-panel-noadbhelper");
+ let noUSBNode = document.querySelector("#runtime-panel-nousbdevice");
+ let noSimulatorNode = document.querySelector("#runtime-panel-nosimulator");
+
+ if (Devices.helperAddonInstalled) {
+ noHelperNode.setAttribute("hidden", "true");
+ } else {
+ noHelperNode.removeAttribute("hidden");
+ }
+
+ if (AppManager.runtimeList.usb.length == 0 && Devices.helperAddonInstalled) {
+ noUSBNode.removeAttribute("hidden");
+ } else {
+ noUSBNode.setAttribute("hidden", "true");
+ }
+
+ if (AppManager.runtimeList.simulator.length > 0) {
+ noSimulatorNode.setAttribute("hidden", "true");
+ } else {
+ noSimulatorNode.removeAttribute("hidden");
+ }
+
for (let [type, parent] of [
["usb", USBListNode],
["simulator", simulatorListNode],
@@ -283,7 +326,7 @@ let UI = {
return this.projecteditor.loaded;
}
- let projecteditorIframe = document.querySelector("#projecteditor");
+ let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe);
this.projecteditor.on("onEditorSave", (editor, resource) => {
AppManager.validateProject(AppManager.selectedProject);
@@ -306,7 +349,7 @@ let UI = {
iconUrl: project.icon,
projectOverviewURL: "chrome://webide/content/details.xhtml",
validationStatus: status
- });
+ }).then(null, console.error);
}, console.error);
},
@@ -315,17 +358,12 @@ let UI = {
},
openProject: function() {
- let detailsIframe = document.querySelector("#details");
- let projecteditorIframe = document.querySelector("#projecteditor");
-
let project = AppManager.selectedProject;
// Nothing to show
if (!project) {
- detailsIframe.setAttribute("hidden", "true");
- projecteditorIframe.setAttribute("hidden", "true");
- document.commandDispatcher.focusedElement = document.documentElement;
+ this.resetDeck();
return;
}
@@ -342,16 +380,13 @@ let UI = {
if (project.type != "packaged" ||
!this.isProjectEditorEnabled() ||
forceDetailsOnly) {
- detailsIframe.removeAttribute("hidden");
- projecteditorIframe.setAttribute("hidden", "true");
- document.commandDispatcher.focusedElement = document.documentElement;
+ this.selectDeckPanel("details");
return;
}
// Show ProjectEditor
- detailsIframe.setAttribute("hidden", "true");
- projecteditorIframe.removeAttribute("hidden");
+ this.selectDeckPanel("projecteditor");
this.getProjectEditor().then(() => {
this.updateProjectEditorHeader();
@@ -362,6 +397,26 @@ let UI = {
}
},
+ /********** DECK **********/
+
+ resetFocus: function() {
+ document.commandDispatcher.focusedElement = document.documentElement;
+ },
+
+ selectDeckPanel: function(id) {
+ this.hidePanels();
+ this.resetFocus();
+ let deck = document.querySelector("#deck");
+ let panel = deck.querySelector("#deck-panel-" + id);
+ deck.selectedPanel = panel;
+ },
+
+ resetDeck: function() {
+ this.resetFocus();
+ let deck = document.querySelector("#deck");
+ deck.selectedPanel = null;
+ },
+
/********** COMMANDS **********/
updateCommands: function() {
@@ -506,9 +561,7 @@ let UI = {
},
closeToolboxUI: function() {
- let body = document.querySelector("#body");
- body.removeAttribute("hidden");
-
+ this.resetFocus();
Services.prefs.setIntPref("devtools.toolbox.footer.height", this.toolboxIframe.height);
// We have to destroy the iframe, otherwise, the keybindings of webide don't work
@@ -727,81 +780,11 @@ let Cmds = {
},
showPermissionsTable: function() {
- return UI.busyUntil(AppManager.deviceFront.getRawPermissionsTable().then(json => {
- let styleContent = "";
- styleContent += "body {background:white; font-family: monospace}";
- styleContent += "table {border-collapse: collapse}";
- styleContent += "th, td {padding: 5px; border: 1px solid #EEE}";
- styleContent += "th {min-width: 130px}";
- styleContent += "td {text-align: center}";
- styleContent += "th:first-of-type, td:first-of-type {text-align:left}";
- styleContent += ".permallow {color:rgb(152, 207, 57)}";
- styleContent += ".permprompt {color:rgb(0,158,237)}";
- styleContent += ".permdeny {color:rgb(204,73,8)}";
- let style = document.createElementNS(HTML, "style");
- style.textContent = styleContent;
- let table = document.createElementNS(HTML, "table");
- table.innerHTML = "Name type:web type:privileged type:certified ";
- let permissionsTable = json.rawPermissionsTable;
- for (let name in permissionsTable) {
- let tr = document.createElementNS(HTML, "tr");
- let td = document.createElementNS(HTML, "td");
- td.textContent = name;
- tr.appendChild(td);
- for (let type of ["app","privileged","certified"]) {
- let td = document.createElementNS(HTML, "td");
- if (permissionsTable[name][type] == json.ALLOW_ACTION) {
- td.textContent = "✓";
- td.className = "permallow";
- }
- if (permissionsTable[name][type] == json.PROMPT_ACTION) {
- td.textContent = "!";
- td.className = "permprompt";
- }
- if (permissionsTable[name][type] == json.DENY_ACTION) {
- td.textContent = "✕";
- td.className = "permdeny"
- }
- tr.appendChild(td);
- }
- table.appendChild(tr);
- }
- let body = document.createElementNS(HTML, "body");
- body.appendChild(style);
- body.appendChild(table);
- let url = "data:text/html;charset=utf-8,";
- url += encodeURIComponent(body.outerHTML);
- UI.openInBrowser(url);
- }), "showing permission table");
+ UI.selectDeckPanel("permissionstable");
},
showRuntimeDetails: function() {
- return UI.busyUntil(AppManager.deviceFront.getDescription().then(json => {
- let styleContent = "";
- styleContent += "body {background:white; font-family: monospace}";
- styleContent += "table {border-collapse: collapse}";
- styleContent += "th, td {padding: 5px; border: 1px solid #EEE}";
- let style = document.createElementNS(HTML, "style");
- style.textContent = styleContent;
- let table = document.createElementNS(HTML, "table");
- for (let name in json) {
- let tr = document.createElementNS(HTML, "tr");
- let td = document.createElementNS(HTML, "td");
- td.textContent = name;
- tr.appendChild(td);
- td = document.createElementNS(HTML, "td");
- td.textContent = json[name];
- tr.appendChild(td);
- table.appendChild(tr);
- }
- let body = document.createElementNS(HTML, "body");
- body.appendChild(style);
- body.appendChild(table);
- let url = "data:text/html;charset=utf-8,";
- url += encodeURIComponent(body.outerHTML);
- UI.openInBrowser(url);
- }), "showing runtime details");
-
+ UI.selectDeckPanel("runtimedetails");
},
play: function() {
@@ -846,4 +829,8 @@ let Cmds = {
showTroubleShooting: function() {
UI.openInBrowser(HELP_URL);
},
+
+ showAddons: function() {
+ UI.selectDeckPanel("addons");
+ },
}
diff --git a/browser/devtools/webide/content/webide.xul b/browser/devtools/webide/content/webide.xul
index 4d3da7984236..3a77c8eb22eb 100644
--- a/browser/devtools/webide/content/webide.xul
+++ b/browser/devtools/webide/content/webide.xul
@@ -41,6 +41,8 @@
+
+
@@ -76,6 +78,7 @@
@@ -136,8 +139,11 @@
+
+
+
@@ -151,9 +157,14 @@
-
-
-
+
+
+
+
+
+
+
+
diff --git a/browser/devtools/webide/locales/en-US/webide.dtd b/browser/devtools/webide/locales/en-US/webide.dtd
index 252070c4b46c..959106b6480b 100644
--- a/browser/devtools/webide/locales/en-US/webide.dtd
+++ b/browser/devtools/webide/locales/en-US/webide.dtd
@@ -38,6 +38,8 @@
+
+
@@ -61,6 +63,9 @@
+
+
+
@@ -76,3 +81,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/devtools/webide/locales/en-US/webide.properties b/browser/devtools/webide/locales/en-US/webide.properties
index acefe3af9f23..f0a74bd47f9f 100644
--- a/browser/devtools/webide/locales/en-US/webide.properties
+++ b/browser/devtools/webide/locales/en-US/webide.properties
@@ -26,3 +26,18 @@ error_listRunningApps=Can't get app list from device
error_cantConnectToApp=Can't connect to app: %1$S
error_cantInstallNotFullyConnected=Can't install project. Not fully connected.
error_cantInstallValidationErrors=Can't install project. Validation errors.
+error_cantFetchAddonsJSON=Can't fetch the addon list: %S
+
+addons_stable=stable
+addons_unstable=unstable
+addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
+addons_install_button=install
+addons_uninstall_button=uninstall
+addons_adb_label=ADB Addon Helper
+addons_adb_warning=USB devices won't be detected without this add-on
+addons_status_unknown=?
+addons_status_installed=Installed
+addons_status_uninstalled=Not Installed
+addons_status_preparing=preparing
+addons_status_downloading=downloading
+addons_status_installing=installing
diff --git a/browser/devtools/webide/modules/addons.js b/browser/devtools/webide/modules/addons.js
new file mode 100644
index 000000000000..d90ebd418feb
--- /dev/null
+++ b/browser/devtools/webide/modules/addons.js
@@ -0,0 +1,206 @@
+/* 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 {Cu} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm");
+const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm");
+const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
+const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
+const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+const {GetAddonsJSON} = require("devtools/webide/remote-resources");
+
+let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
+let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
+let SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
+let ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
+
+let platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+let OS = "";
+if (platform.indexOf("Win") != -1) {
+ OS = "win32";
+} else if (platform.indexOf("Mac") != -1) {
+ OS = "mac64";
+} else if (platform.indexOf("Linux") != -1) {
+ if (platform.indexOf("x86_64") != -1) {
+ OS = "linux64";
+ } else {
+ OS = "linux";
+ }
+}
+
+Simulator.on("unregister", updateSimulatorAddons);
+Simulator.on("register", updateSimulatorAddons);
+Devices.on("addon-status-updated", updateAdbAddon);
+
+function updateSimulatorAddons(event, version) {
+ GetAvailableAddons().then(addons => {
+ let foundAddon = null;
+ for (let addon of addons.simulators) {
+ if (addon.version == version) {
+ foundAddon = addon;
+ break;
+ }
+ }
+ if (!foundAddon) {
+ console.warn("An unknown simulator (un)registered", version);
+ return;
+ }
+ foundAddon.updateInstallStatus();
+ });
+}
+
+function updateAdbAddon() {
+ GetAvailableAddons().then(addons => {
+ addons.adb.updateInstallStatus();
+ });
+}
+
+let GetAvailableAddons_promise = null;
+let GetAvailableAddons = exports.GetAvailableAddons = function() {
+ if (!GetAvailableAddons_promise) {
+ let deferred = promise.defer();
+ GetAvailableAddons_promise = deferred.promise;
+ let addons = {
+ simulators: [],
+ adb: null
+ }
+ GetAddonsJSON().then(json => {
+ for (let stability in json) {
+ for (let version of json[stability]) {
+ addons.simulators.push(new SimulatorAddon(stability, version));
+ }
+ }
+ addons.adb = new ADBAddon();
+ deferred.resolve(addons);
+ }, e => {
+ GetAvailableAddons_promise = null;
+ deferred.reject(e);
+ });
+ }
+ return GetAvailableAddons_promise;
+}
+
+function Addon() {}
+Addon.prototype = {
+ _status: "unknown",
+ set status(value) {
+ if (this._status != value) {
+ this._status = value;
+ this.emit("update");
+ }
+ },
+ get status() {
+ return this._status;
+ },
+
+ install: function() {
+ if (this.status != "uninstalled") {
+ throw new Error("Not uninstalled");
+ }
+ this.status = "preparing";
+
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && addon.userDisabled) {
+ addon.userDisabled = false;
+ } else {
+ AddonManager.getInstallForURL(this.xpiLink, (install) => {
+ install.addListener(this);
+ install.install();
+ }, "application/x-xpinstall");
+ }
+ });
+
+ },
+
+ uninstall: function() {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ addon.uninstall();
+ });
+ },
+
+ installFailureHandler: function(install, message) {
+ this.status = "uninstalled";
+ this.emit("failure", message);
+ },
+
+ onDownloadStarted: function() {
+ this.status = "downloading";
+ },
+
+ onInstallStarted: function() {
+ this.status = "installing";
+ },
+
+ onDownloadProgress: function(install) {
+ if (install.maxProgress == -1) {
+ this.emit("progress", -1);
+ } else {
+ this.emit("progress", install.progress / install.maxProgress);
+ }
+ },
+
+ onInstallEnded: function({addon}) {
+ addon.userDisabled = false;
+ },
+
+ onDownloadCancelled: function(install) {
+ this.installFailureHandler(install, "Download cancelled");
+ },
+ onDownloadFailed: function(install) {
+ this.installFailureHandler(install, "Download failed");
+ },
+ onInstallCancelled: function(install) {
+ this.installFailureHandler(install, "Install cancelled");
+ },
+ onInstallFailed: function(install) {
+ this.installFailureHandler(install, "Install failed");
+ },
+}
+
+function SimulatorAddon(stability, version) {
+ EventEmitter.decorate(this);
+ this.stability = stability;
+ this.version = version;
+ this.xpiLink = SIMULATOR_LINK.replace(/#OS#/g, OS)
+ .replace(/#VERSION#/g, version)
+ .replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.addonID = SIMULATOR_ADDON_ID.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.updateInstallStatus();
+}
+
+SimulatorAddon.prototype = Object.create(Addon.prototype, {
+ updateInstallStatus: {
+ enumerable: true,
+ value: function() {
+ let sim = Simulator.getByVersion(this.version);
+ if (sim) {
+ this.status = "installed";
+ } else {
+ this.status = "uninstalled";
+ }
+ }
+ },
+});
+
+function ADBAddon() {
+ EventEmitter.decorate(this);
+ this.xpiLink = ADB_LINK.replace(/#OS#/g, OS);
+ this.addonID = ADB_ADDON_ID;
+ this.updateInstallStatus();
+}
+
+ADBAddon.prototype = Object.create(Addon.prototype, {
+ updateInstallStatus: {
+ enumerable: true,
+ value: function() {
+ if (Devices.helperAddonInstalled) {
+ this.status = "installed";
+ } else {
+ this.status = "uninstalled";
+ }
+ }
+ },
+});
+
diff --git a/browser/devtools/webide/modules/remote-resources.js b/browser/devtools/webide/modules/remote-resources.js
new file mode 100644
index 000000000000..9a64a84fc321
--- /dev/null
+++ b/browser/devtools/webide/modules/remote-resources.js
@@ -0,0 +1,54 @@
+/* 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 {Cu, CC} = require("chrome");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+const {Services} = Cu.import("resource://gre/modules/Services.jsm");
+
+const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+function getJSON(bypassCache, pref) {
+ if (!bypassCache) {
+ try {
+ let str = Services.prefs.getCharPref(pref + "_cache");
+ let json = JSON.parse(str);
+ return promise.resolve(json);
+ } catch(e) {/* no pref or invalid json. Let's continue */}
+ }
+
+
+ let deferred = promise.defer();
+
+ let xhr = new XMLHttpRequest();
+
+ xhr.onload = () => {
+ let json;
+ try {
+ json = JSON.parse(xhr.responseText);
+ } catch(e) {
+ return deferred.reject("Not valid JSON");
+ }
+ Services.prefs.setCharPref(pref + "_cache", xhr.responseText);
+ deferred.resolve(json);
+ }
+
+ xhr.onerror = (e) => {
+ deferred.reject("Network error");
+ }
+
+ xhr.open("get", Services.prefs.getCharPref(pref));
+ xhr.send();
+
+ return deferred.promise;
+}
+
+
+
+exports.GetTemplatesJSON = function(bypassCache) {
+ return getJSON(bypassCache, "devtools.webide.templatesURL");
+}
+
+exports.GetAddonsJSON = function(bypassCache) {
+ return getJSON(bypassCache, "devtools.webide.addonsURL");
+}
diff --git a/browser/devtools/webide/modules/runtimes.js b/browser/devtools/webide/modules/runtimes.js
index 8ce6e5f182c0..62798831b67b 100644
--- a/browser/devtools/webide/modules/runtimes.js
+++ b/browser/devtools/webide/modules/runtimes.js
@@ -56,12 +56,11 @@ SimulatorRuntime.prototype = {
return this.version;
},
getName: function() {
- return this.version;
+ return Simulator.getByVersion(this.version).appinfo.label;
},
}
let gLocalRuntime = {
- supportApps: false, // Temporary static value
connect: function(connection) {
if (!DebuggerServer.initialized) {
DebuggerServer.init();
diff --git a/browser/devtools/webide/test/addons/adbhelper-linux.xpi b/browser/devtools/webide/test/addons/adbhelper-linux.xpi
new file mode 100644
index 000000000000..b56cc03e349e
Binary files /dev/null and b/browser/devtools/webide/test/addons/adbhelper-linux.xpi differ
diff --git a/browser/devtools/webide/test/addons/adbhelper-linux64.xpi b/browser/devtools/webide/test/addons/adbhelper-linux64.xpi
new file mode 100644
index 000000000000..b56cc03e349e
Binary files /dev/null and b/browser/devtools/webide/test/addons/adbhelper-linux64.xpi differ
diff --git a/browser/devtools/webide/test/addons/adbhelper-mac64.xpi b/browser/devtools/webide/test/addons/adbhelper-mac64.xpi
new file mode 100644
index 000000000000..b56cc03e349e
Binary files /dev/null and b/browser/devtools/webide/test/addons/adbhelper-mac64.xpi differ
diff --git a/browser/devtools/webide/test/addons/adbhelper-win32.xpi b/browser/devtools/webide/test/addons/adbhelper-win32.xpi
new file mode 100644
index 000000000000..b56cc03e349e
Binary files /dev/null and b/browser/devtools/webide/test/addons/adbhelper-win32.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_1_0_simulator-linux.xpi b/browser/devtools/webide/test/addons/fxos_1_0_simulator-linux.xpi
new file mode 100644
index 000000000000..81e1abc6e1d3
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_1_0_simulator-linux.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_1_0_simulator-linux64.xpi b/browser/devtools/webide/test/addons/fxos_1_0_simulator-linux64.xpi
new file mode 100644
index 000000000000..81e1abc6e1d3
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_1_0_simulator-linux64.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_1_0_simulator-mac64.xpi b/browser/devtools/webide/test/addons/fxos_1_0_simulator-mac64.xpi
new file mode 100644
index 000000000000..81e1abc6e1d3
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_1_0_simulator-mac64.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_1_0_simulator-win32.xpi b/browser/devtools/webide/test/addons/fxos_1_0_simulator-win32.xpi
new file mode 100644
index 000000000000..81e1abc6e1d3
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_1_0_simulator-win32.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_2_0_simulator-linux.xpi b/browser/devtools/webide/test/addons/fxos_2_0_simulator-linux.xpi
new file mode 100644
index 000000000000..1ce3f959e1a1
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_2_0_simulator-linux.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_2_0_simulator-linux64.xpi b/browser/devtools/webide/test/addons/fxos_2_0_simulator-linux64.xpi
new file mode 100644
index 000000000000..1ce3f959e1a1
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_2_0_simulator-linux64.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_2_0_simulator-mac64.xpi b/browser/devtools/webide/test/addons/fxos_2_0_simulator-mac64.xpi
new file mode 100644
index 000000000000..1ce3f959e1a1
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_2_0_simulator-mac64.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_2_0_simulator-win32.xpi b/browser/devtools/webide/test/addons/fxos_2_0_simulator-win32.xpi
new file mode 100644
index 000000000000..1ce3f959e1a1
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_2_0_simulator-win32.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_3_0_simulator-linux.xpi b/browser/devtools/webide/test/addons/fxos_3_0_simulator-linux.xpi
new file mode 100644
index 000000000000..ec9645da40d5
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_3_0_simulator-linux.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_3_0_simulator-linux64.xpi b/browser/devtools/webide/test/addons/fxos_3_0_simulator-linux64.xpi
new file mode 100644
index 000000000000..ec9645da40d5
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_3_0_simulator-linux64.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_3_0_simulator-mac64.xpi b/browser/devtools/webide/test/addons/fxos_3_0_simulator-mac64.xpi
new file mode 100644
index 000000000000..ec9645da40d5
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_3_0_simulator-mac64.xpi differ
diff --git a/browser/devtools/webide/test/addons/fxos_3_0_simulator-win32.xpi b/browser/devtools/webide/test/addons/fxos_3_0_simulator-win32.xpi
new file mode 100644
index 000000000000..ec9645da40d5
Binary files /dev/null and b/browser/devtools/webide/test/addons/fxos_3_0_simulator-win32.xpi differ
diff --git a/browser/devtools/webide/test/addons/simulators.json b/browser/devtools/webide/test/addons/simulators.json
new file mode 100644
index 000000000000..dace8c8a93e0
--- /dev/null
+++ b/browser/devtools/webide/test/addons/simulators.json
@@ -0,0 +1,4 @@
+{
+ "stable": ["1.0", "2.0"],
+ "unstable": ["3.0"]
+}
diff --git a/browser/devtools/webide/test/chrome.ini b/browser/devtools/webide/test/chrome.ini
index af762c858590..7092d0bc4429 100644
--- a/browser/devtools/webide/test/chrome.ini
+++ b/browser/devtools/webide/test/chrome.ini
@@ -3,6 +3,23 @@ support-files =
app/index.html
app/manifest.webapp
app.zip
+ addons/simulators.json
+ addons/fxos_1_0_simulator-linux.xpi
+ addons/fxos_1_0_simulator-linux64.xpi
+ addons/fxos_1_0_simulator-win32.xpi
+ addons/fxos_1_0_simulator-mac64.xpi
+ addons/fxos_2_0_simulator-linux.xpi
+ addons/fxos_2_0_simulator-linux64.xpi
+ addons/fxos_2_0_simulator-win32.xpi
+ addons/fxos_2_0_simulator-mac64.xpi
+ addons/fxos_3_0_simulator-linux.xpi
+ addons/fxos_3_0_simulator-linux64.xpi
+ addons/fxos_3_0_simulator-win32.xpi
+ addons/fxos_3_0_simulator-mac64.xpi
+ addons/adbhelper-linux.xpi
+ addons/adbhelper-linux64.xpi
+ addons/adbhelper-win32.xpi
+ addons/adbhelper-mac64.xpi
head.js
hosted_app.manifest
templates.json
@@ -13,3 +30,5 @@ support-files =
[test_runtime.html]
[test_cli.html]
[test_manifestUpdate.html]
+[test_addons.html]
+[test_deviceinfo.html]
diff --git a/browser/devtools/webide/test/head.js b/browser/devtools/webide/test/head.js
index c618e782c3cd..7c4938d861cc 100644
--- a/browser/devtools/webide/test/head.js
+++ b/browser/devtools/webide/test/head.js
@@ -19,14 +19,29 @@ const TEST_BASE = "chrome://mochitests/content/chrome/browser/devtools/webide/te
Services.prefs.setBoolPref("devtools.webide.enabled", true);
Services.prefs.setBoolPref("devtools.webide.enableLocalRuntime", true);
+Services.prefs.setCharPref("devtools.webide.addonsURL", TEST_BASE + "addons/simulators.json");
+Services.prefs.setCharPref("devtools.webide.simulatorAddonsURL", TEST_BASE + "addons/fxos_#SLASHED_VERSION#_simulator-#OS#.xpi");
+Services.prefs.setCharPref("devtools.webide.adbAddonURL", TEST_BASE + "addons/adbhelper-#OS#.xpi");
+Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
+
+
SimpleTest.registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("devtools.webide.templatesURL");
Services.prefs.clearUserPref("devtools.webide.enabled");
Services.prefs.clearUserPref("devtools.webide.enableLocalRuntime");
+ Services.prefs.clearUserPref("devtools.webide.addonsURL");
+ Services.prefs.clearUserPref("devtools.webide.simulatorAddonsURL");
+ Services.prefs.clearUserPref("devtools.webide.adbAddonURL");
+ Services.prefs.clearUserPref("devtools.webide.autoInstallADBHelper", false);
});
-function openWebIDE() {
+function openWebIDE(autoInstallADBHelper) {
info("opening WebIDE");
+ if (!autoInstallADBHelper) {
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+ }
+
let deferred = promise.defer();
let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
@@ -80,3 +95,18 @@ function nextTick() {
return deferred.promise;
}
+
+function documentIsLoaded(doc) {
+ let deferred = promise.defer();
+ if (doc.readyState == "complete") {
+ deferred.resolve();
+ } else {
+ doc.addEventListener("readystatechange", function onChange() {
+ if (doc.readyState == "complete") {
+ doc.removeEventListener("readystatechange", onChange);
+ deferred.resolve();
+ }
+ });
+ }
+ return deferred.promise;
+}
diff --git a/browser/devtools/webide/test/test_addons.html b/browser/devtools/webide/test/test_addons.html
new file mode 100644
index 000000000000..24ec846c97c8
--- /dev/null
+++ b/browser/devtools/webide/test/test_addons.html
@@ -0,0 +1,168 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/devtools/webide/test/test_basic.html b/browser/devtools/webide/test/test_basic.html
index 4e15d2a77111..b0b01df11c9d 100644
--- a/browser/devtools/webide/test/test_basic.html
+++ b/browser/devtools/webide/test/test_basic.html
@@ -29,7 +29,7 @@
ok(appmgr.webAppsStore, "WebApps store ready");
// test error reporting
- let nbox = win.document.querySelector("#body");
+ let nbox = win.document.querySelector("#notificationbox");
let notification = nbox.getNotificationWithValue("webide:errornotification");
ok(!notification, "No notification yet");
let deferred = promise.defer();
diff --git a/browser/devtools/webide/test/test_deviceinfo.html b/browser/devtools/webide/test/test_deviceinfo.html
new file mode 100644
index 000000000000..31a10021bd5f
--- /dev/null
+++ b/browser/devtools/webide/test/test_deviceinfo.html
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/devtools/webide/test/test_newapp.html b/browser/devtools/webide/test/test_newapp.html
index fef8ff2e421e..c727507ae01c 100644
--- a/browser/devtools/webide/test/test_newapp.html
+++ b/browser/devtools/webide/test/test_newapp.html
@@ -18,8 +18,6 @@
window.onload = function() {
SimpleTest.waitForExplicitFinish();
- Services.prefs.setCharPref("devtools.webide.templatesURL", TEST_BASE + "templates.json");
-
Task.spawn(function* () {
let win = yield openWebIDE();
let tmpDir = FileUtils.getDir("TmpD", []);
@@ -37,7 +35,6 @@
// Clean up
tmpDir.remove(true);
- Services.prefs.clearUserPref("devtools.webide.templatesURL");
yield closeWebIDE(win);
yield removeAllProjects();
SimpleTest.finish();
diff --git a/browser/devtools/webide/themes/addons.css b/browser/devtools/webide/themes/addons.css
new file mode 100644
index 000000000000..1f362ea34573
--- /dev/null
+++ b/browser/devtools/webide/themes/addons.css
@@ -0,0 +1,121 @@
+/* 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/. */
+
+@import url("chrome://browser/skin/in-content/common.css");
+
+html {
+ font: message-box;
+ font-size: 15px;
+ font-weight: normal;
+ margin: 0;
+ color: #737980;
+ background-image: linear-gradient(#fff, #ededed 100px);
+ height: 100%;
+}
+
+body {
+ padding: 20px;
+}
+
+h1 {
+ font-size: 2.5em;
+ font-weight: lighter;
+ line-height: 1.2;
+ margin: 0;
+ margin-bottom: .5em;
+}
+
+button {
+ line-height: 20px;
+ font-size: 1em;
+ height: 30px;
+ max-height: 30px;
+ min-width: 120px;
+ padding: 3px;
+ color: #737980;
+ border: 1px solid rgba(23,50,77,.4);
+ border-radius: 5px;
+ background-color: #f1f1f1;
+ background-image: linear-gradient(#fff, rgba(255,255,255,.1));
+ box-shadow: 0 1px 1px 0 #fff, inset 0 2px 2px 0 #fff;
+ text-shadow: 0 1px 1px #fefffe;
+ -moz-appearance: none;
+ -moz-border-top-colors: none !important;
+ -moz-border-right-colors: none !important;
+ -moz-border-bottom-colors: none !important;
+ -moz-border-left-colors: none !important;
+}
+
+button:hover {
+ background-image: linear-gradient(#fff, rgba(255,255,255,.6));
+ cursor: pointer;
+}
+
+button:hover:active {
+ background-image: linear-gradient(rgba(255,255,255,.1), rgba(255,255,255,.6));
+}
+
+progress {
+ height: 30px;
+ vertical-align: middle;
+ padding: 0;
+ width: 120px;
+}
+
+li {
+ margin: 20px 0;
+}
+
+.name {
+ display: inline-block;
+ min-width: 280px;
+}
+
+.status {
+ display: inline-block;
+ min-width: 120px;
+}
+
+.warning {
+ color: #F06;
+ margin: 0;
+ font-size: 0.9em;
+}
+
+
+#controls {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+}
+
+#controls > a {
+ color: #4C9ED9;
+ font-size: small;
+ cursor: pointer;
+ border-bottom: 1px dotted;
+}
+
+#close {
+ margin-left: 10px;
+}
+
+li[status="unknown"],
+li > .uninstall-button,
+li > .install-button,
+li > progress {
+ display: none;
+}
+
+li[status="installed"] > .uninstall-button,
+li[status="uninstalled"] > .install-button,
+li[status="preparing"] > progress,
+li[status="downloading"] > progress,
+li[status="installing"] > progress {
+ display: inline;
+}
+
+li:not([status="uninstalled"]) > .warning {
+ display: none;
+}
diff --git a/browser/devtools/webide/themes/jar.mn b/browser/devtools/webide/themes/jar.mn
index 6b39402cc30b..1eb33bbf2ec4 100644
--- a/browser/devtools/webide/themes/jar.mn
+++ b/browser/devtools/webide/themes/jar.mn
@@ -9,3 +9,5 @@ webide.jar:
skin/details.css (details.css)
skin/newapp.css (newapp.css)
skin/throbber.svg (throbber.svg)
+ skin/addons.css (addons.css)
+ skin/tabledoc.css (tabledoc.css)
diff --git a/browser/devtools/webide/themes/tabledoc.css b/browser/devtools/webide/themes/tabledoc.css
new file mode 100644
index 000000000000..46ef9d2272e1
--- /dev/null
+++ b/browser/devtools/webide/themes/tabledoc.css
@@ -0,0 +1,54 @@
+body {
+ background: white;
+}
+
+#controls {
+ position: fixed;
+ top: 10px;
+ right: 10px;
+}
+
+#controls > a {
+ color: #4C9ED9;
+ font-size: small;
+ cursor: pointer;
+ border-bottom: 1px dotted;
+}
+
+#close {
+ margin-left: 10px;
+}
+
+table {
+ font-family: monospace;
+ border-collapse: collapse;
+}
+
+th, td {
+ padding: 5px;
+ border: 1px solid #EEE;
+}
+
+th {
+ min-width: 130px;
+}
+
+.permissionstable td {
+ text-align: center;
+}
+
+th:first-of-type, td:first-of-type {
+ text-align: left;
+}
+
+.permallow {
+ color: rgb(152,207,57);
+}
+
+.permprompt {
+ color: rgb(0,158,237);
+}
+
+.permdeny {
+ color: rgb(204,73,8);
+}
diff --git a/browser/devtools/webide/themes/webide.css b/browser/devtools/webide/themes/webide.css
index e8971d6eac64..e97e88e72fa4 100644
--- a/browser/devtools/webide/themes/webide.css
+++ b/browser/devtools/webide/themes/webide.css
@@ -40,7 +40,7 @@ window:not(.busy) #action-busy {
.panel-button-anchor {
list-style-image: url('icons.png');
-moz-image-region: rect(43px, 563px, 61px, 535px);
- width: 12;
+ width: 12px;
height: 7px;
margin-bottom: -5px;
}
@@ -118,13 +118,19 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
width: 180px;
}
-.panel-item {
+.panel-item,
+.panel-item-help {
padding: 3px 12px;
margin: 0;
-moz-appearance: none;
}
-.panel-item:hover {
+.panel-item-help {
+ font-size: 0.9em;
+}
+
+.panel-item:hover,
+.panel-item-help:hover {
background: #CBF0FE;
}
@@ -151,7 +157,8 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
height: 18px;
}
-.panel-item > .toolbarbutton-text {
+.panel-item > .toolbarbutton-text,
+.panel-item-help > .toolbarbutton-text {
text-align: start;
}
@@ -214,7 +221,7 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
/* Main view */
-#body {
+#deck {
background-color: rgb(225, 225, 225);
background-image: url('chrome://browser/skin/devtools/app-manager/rocket.svg'), url('chrome://browser/skin/devtools/app-manager/noise.png');
background-repeat: no-repeat, repeat;
diff --git a/browser/devtools/webide/webide-prefs.js b/browser/devtools/webide/webide-prefs.js
index 4a938bf73a5a..f0bf6338baae 100644
--- a/browser/devtools/webide/webide-prefs.js
+++ b/browser/devtools/webide/webide-prefs.js
@@ -4,6 +4,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
pref("devtools.webide.showProjectEditor", true);
-pref("devtools.webide.templatesURL", "http://people.mozilla.org/~prouget/webidetemplates/template.json"); // See bug 1021504
+pref("devtools.webide.templatesURL", "http://code.cdn.mozilla.net/templates/list.json");
+pref("devtools.webide.autoinstallADBHelper", true);
pref("devtools.webide.lastprojectlocation", "");
pref("devtools.webide.enableLocalRuntime", false);
+pref("devtools.webide.addonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/index.json");
+pref("devtools.webide.simulatorAddonsURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/#VERSION#/#OS#/fxos_#SLASHED_VERSION#_simulator-#OS#-latest.xpi");
+pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozilla.org");
+pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
+pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
diff --git a/browser/themes/linux/browser.css b/browser/themes/linux/browser.css
index 5f1c1829971c..759221adca09 100644
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1822,19 +1822,39 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
}
/* Tabbrowser arrowscrollbox arrows */
+.tabbrowser-arrowscrollbox > .scrollbutton-up > .toolbarbutton-icon,
+.tabbrowser-arrowscrollbox > .scrollbutton-down > .toolbarbutton-icon {
+ -moz-appearance: none;
+}
+
.tabbrowser-arrowscrollbox > .scrollbutton-up,
.tabbrowser-arrowscrollbox > .scrollbutton-down {
-moz-appearance: none;
+ list-style-image: url("chrome://browser/skin/tabbrowser/tab-arrow-left.png");
margin: 0 0 @tabToolbarNavbarOverlap@;
}
+#TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-up,
+#TabsToolbar[brighttext] > #tabbrowser-tabs > .tabbrowser-arrowscrollbox > .scrollbutton-down {
+ list-style-image: url(chrome://browser/skin/tabbrowser/tab-arrow-left-inverted.png);
+}
+
+.tabbrowser-arrowscrollbox > .scrollbutton-up[disabled],
+.tabbrowser-arrowscrollbox > .scrollbutton-down[disabled] {
+ opacity: .4;
+}
+
+.tabbrowser-arrowscrollbox > .scrollbutton-up:-moz-locale-dir(rtl),
+.tabbrowser-arrowscrollbox > .scrollbutton-down:-moz-locale-dir(ltr) {
+ transform: scaleX(-1);
+}
+
.tabbrowser-arrowscrollbox > .scrollbutton-down {
- transition: 1s box-shadow ease-out;
- border-radius: 4px;
+ transition: 1s background-color ease-out;
}
.tabbrowser-arrowscrollbox > .scrollbutton-down[notifybgtab] {
- box-shadow: 0 0 5px 5px Highlight inset;
+ background-color: Highlight;
transition: none;
}
diff --git a/browser/themes/linux/jar.mn b/browser/themes/linux/jar.mn
index 96ba8d6d22f1..ece7d33d0d63 100644
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -163,6 +163,8 @@ browser.jar:
skin/classic/browser/tabbrowser/connecting.png (tabbrowser/connecting.png)
skin/classic/browser/tabbrowser/loading.png (tabbrowser/loading.png)
skin/classic/browser/tabbrowser/tab-active-middle.png (tabbrowser/tab-active-middle.png)
+ skin/classic/browser/tabbrowser/tab-arrow-left.png (tabbrowser/tab-arrow-left.png)
+ skin/classic/browser/tabbrowser/tab-arrow-left-inverted.png (tabbrowser/tab-arrow-left-inverted.png)
skin/classic/browser/tabbrowser/tab-background-end.png (tabbrowser/tab-background-end.png)
skin/classic/browser/tabbrowser/tab-background-middle.png (tabbrowser/tab-background-middle.png)
skin/classic/browser/tabbrowser/tab-background-start.png (tabbrowser/tab-background-start.png)
diff --git a/browser/themes/linux/tabbrowser/tab-arrow-left-inverted.png b/browser/themes/linux/tabbrowser/tab-arrow-left-inverted.png
new file mode 100644
index 000000000000..16cd7a2775ae
Binary files /dev/null and b/browser/themes/linux/tabbrowser/tab-arrow-left-inverted.png differ
diff --git a/browser/themes/linux/tabbrowser/tab-arrow-left.png b/browser/themes/linux/tabbrowser/tab-arrow-left.png
new file mode 100644
index 000000000000..e0fb348d66f4
Binary files /dev/null and b/browser/themes/linux/tabbrowser/tab-arrow-left.png differ
diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java
index 7c6fab1a95eb..fc422547956c 100644
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -2706,20 +2706,24 @@ public class GeckoAppShell
}
@WrapElementForJNI(allowMultithread = true, narrowChars = true)
- static URLConnection getConnection(String url) throws MalformedURLException, IOException {
- String spec;
- if (url.startsWith("android://")) {
- spec = url.substring(10);
- } else {
- spec = url.substring(8);
- }
+ static URLConnection getConnection(String url) {
+ try {
+ String spec;
+ if (url.startsWith("android://")) {
+ spec = url.substring(10);
+ } else {
+ spec = url.substring(8);
+ }
- // if the colon got stripped, put it back
- int colon = spec.indexOf(':');
- if (colon == -1 || colon > spec.indexOf('/')) {
- spec = spec.replaceFirst("/", ":/");
+ // if the colon got stripped, put it back
+ int colon = spec.indexOf(':');
+ if (colon == -1 || colon > spec.indexOf('/')) {
+ spec = spec.replaceFirst("/", ":/");
+ }
+ } catch(Exception ex) {
+ return null;
}
- return new URL(spec).openConnection();
+ return null;
}
@WrapElementForJNI(allowMultithread = true, narrowChars = true)
diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd
index 0da753fe8c6c..4351f68c84f0 100644
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -256,6 +256,7 @@ size. -->
+
diff --git a/mobile/android/base/menu/MenuPopup.java b/mobile/android/base/menu/MenuPopup.java
index d076e48866fb..848587b0cf95 100644
--- a/mobile/android/base/menu/MenuPopup.java
+++ b/mobile/android/base/menu/MenuPopup.java
@@ -20,10 +20,11 @@ import android.widget.PopupWindow;
* A popup to show the inflated MenuPanel.
*/
public class MenuPopup extends PopupWindow {
- private LinearLayout mPanel;
+ private final LinearLayout mPanel;
- private int mYOffset;
- private int mPopupWidth;
+ private final int mYOffset;
+ private final int mPopupWidth;
+ private final int mPopupMinHeight;
public MenuPopup(Context context) {
super(context);
@@ -32,6 +33,7 @@ public class MenuPopup extends PopupWindow {
mYOffset = context.getResources().getDimensionPixelSize(R.dimen.menu_popup_offset);
mPopupWidth = context.getResources().getDimensionPixelSize(R.dimen.menu_popup_width);
+ mPopupMinHeight = context.getResources().getDimensionPixelSize(R.dimen.menu_item_row_height);
// Setting a null background makes the popup to not close on touching outside.
setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
@@ -63,6 +65,12 @@ public class MenuPopup extends PopupWindow {
*/
@Override
public void showAsDropDown(View anchor) {
- showAsDropDown(anchor, 0, -mYOffset);
+ // Set a height, so that the popup will not be displayed below the bottom of the screen.
+ setHeight(mPopupMinHeight);
+
+ // Attempt to align the center of the popup with the center of the anchor. If the anchor is
+ // near the edge of the screen, the popup will just align with the edge of the screen.
+ final int xOffset = anchor.getWidth()/2 - mPopupWidth/2;
+ showAsDropDown(anchor, xOffset, -mYOffset);
}
}
diff --git a/mobile/android/base/resources/drawable-hdpi/menu_tabs.png b/mobile/android/base/resources/drawable-hdpi/menu_tabs.png
new file mode 100644
index 000000000000..af08dc45169a
Binary files /dev/null and b/mobile/android/base/resources/drawable-hdpi/menu_tabs.png differ
diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/menu_tabs.png b/mobile/android/base/resources/drawable-large-hdpi-v11/menu_tabs.png
new file mode 100644
index 000000000000..af08dc45169a
Binary files /dev/null and b/mobile/android/base/resources/drawable-large-hdpi-v11/menu_tabs.png differ
diff --git a/mobile/android/base/resources/drawable-large-mdpi-v11/menu_tabs.png b/mobile/android/base/resources/drawable-large-mdpi-v11/menu_tabs.png
new file mode 100644
index 000000000000..b0210e7cf2c1
Binary files /dev/null and b/mobile/android/base/resources/drawable-large-mdpi-v11/menu_tabs.png differ
diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/menu_tabs.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/menu_tabs.png
new file mode 100644
index 000000000000..6fb585308448
Binary files /dev/null and b/mobile/android/base/resources/drawable-large-xhdpi-v11/menu_tabs.png differ
diff --git a/mobile/android/base/resources/drawable-mdpi/menu_tabs.png b/mobile/android/base/resources/drawable-mdpi/menu_tabs.png
new file mode 100644
index 000000000000..b0210e7cf2c1
Binary files /dev/null and b/mobile/android/base/resources/drawable-mdpi/menu_tabs.png differ
diff --git a/mobile/android/base/resources/drawable-xhdpi/menu_tabs.png b/mobile/android/base/resources/drawable-xhdpi/menu_tabs.png
new file mode 100644
index 000000000000..6fb585308448
Binary files /dev/null and b/mobile/android/base/resources/drawable-xhdpi/menu_tabs.png differ
diff --git a/mobile/android/base/resources/layout-large-land-v11/tabs_panel_footer.xml b/mobile/android/base/resources/layout-large-land-v11/tabs_panel_footer.xml
index 93b5e3f328f5..e66630a09219 100644
--- a/mobile/android/base/resources/layout-large-land-v11/tabs_panel_footer.xml
+++ b/mobile/android/base/resources/layout-large-land-v11/tabs_panel_footer.xml
@@ -6,12 +6,24 @@
+ android:background="@drawable/action_bar_button_inverse"/>
+
+
+
+
diff --git a/mobile/android/base/resources/layout/tabs_panel_header.xml b/mobile/android/base/resources/layout/tabs_panel_header.xml
index 40353035282b..900ff4364761 100644
--- a/mobile/android/base/resources/layout/tabs_panel_header.xml
+++ b/mobile/android/base/resources/layout/tabs_panel_header.xml
@@ -25,4 +25,13 @@
android:contentDescription="@string/new_tab"
android:background="@drawable/action_bar_button_inverse"/>
+
+
diff --git a/mobile/android/base/resources/menu-v11/tabs_menu.xml b/mobile/android/base/resources/menu-v11/tabs_menu.xml
new file mode 100644
index 000000000000..bd974e25b002
--- /dev/null
+++ b/mobile/android/base/resources/menu-v11/tabs_menu.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/base/resources/menu/tabs_menu.xml b/mobile/android/base/resources/menu/tabs_menu.xml
new file mode 100644
index 000000000000..bd974e25b002
--- /dev/null
+++ b/mobile/android/base/resources/menu/tabs_menu.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in
index a2fa7727e444..4dc6ae105c33 100644
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -246,6 +246,7 @@
&new_tab;
&new_private_tab;
&close_all_tabs;
+ &close_private_tabs;
&tabs_normal;
&tabs_private;
&tabs_synced;
diff --git a/mobile/android/base/tabspanel/TabsPanel.java b/mobile/android/base/tabspanel/TabsPanel.java
index b708ad24290b..3e0f5ac7e717 100644
--- a/mobile/android/base/tabspanel/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -13,8 +13,11 @@ import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.LightweightThemeDrawable;
import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.widget.GeckoPopupMenu;
import org.mozilla.gecko.widget.IconTabWidget;
import android.content.Context;
@@ -23,7 +26,10 @@ import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
@@ -32,7 +38,8 @@ import android.widget.LinearLayout;
import android.widget.RelativeLayout;
public class TabsPanel extends LinearLayout
- implements LightweightTheme.OnChangeListener,
+ implements GeckoPopupMenu.OnMenuItemClickListener,
+ LightweightTheme.OnChangeListener,
IconTabWidget.OnTabChangedListener {
@SuppressWarnings("unused")
private static final String LOGTAG = "Gecko" + TabsPanel.class.getSimpleName();
@@ -50,6 +57,10 @@ public class TabsPanel extends LinearLayout
public boolean shouldExpand();
}
+ public static interface CloseAllPanelView {
+ public void closeAll();
+ }
+
public static interface TabsLayoutChangeListener {
public void onTabsLayoutChange(int width, int height);
}
@@ -68,6 +79,7 @@ public class TabsPanel extends LinearLayout
private AppStateListener mAppStateListener;
private IconTabWidget mTabWidget;
+ private static ImageButton mMenuButton;
private static ImageButton mAddTab;
private Panel mCurrentPanel;
@@ -75,6 +87,8 @@ public class TabsPanel extends LinearLayout
private boolean mVisible;
private boolean mHeaderVisible;
+ private GeckoPopupMenu mPopupMenu;
+
public TabsPanel(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
@@ -91,6 +105,10 @@ public class TabsPanel extends LinearLayout
mIsSideBar = false;
+ mPopupMenu = new GeckoPopupMenu(context);
+ mPopupMenu.inflate(R.menu.tabs_menu);
+ mPopupMenu.setOnMenuItemClickListener(this);
+
LayoutInflater.from(context).inflate(R.layout.tabs_panel, this);
initialize();
@@ -149,13 +167,28 @@ public class TabsPanel extends LinearLayout
}
mTabWidget.setTabSelectionListener(this);
+
+ mMenuButton = (ImageButton) findViewById(R.id.menu);
+ mMenuButton.setOnClickListener(new Button.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ final Menu menu = mPopupMenu.getMenu();
+ menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
+ menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS);
+
+ mPopupMenu.show();
+ }
+ });
+ mPopupMenu.setAnchor(mMenuButton);
}
- public void addTab() {
+ private void addTab() {
if (mCurrentPanel == Panel.NORMAL_TABS) {
- mActivity.addTab();
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_tab");
+ mActivity.addTab();
} else {
- mActivity.addPrivateTab();
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.ACTIONBAR, "new_private_tab");
+ mActivity.addPrivateTab();
}
mActivity.autoHideTabs();
@@ -172,6 +205,43 @@ public class TabsPanel extends LinearLayout
}
}
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ final int itemId = item.getItemId();
+
+ if (itemId == R.id.close_all_tabs) {
+ if (mCurrentPanel == Panel.NORMAL_TABS) {
+ final String extras = getResources().getResourceEntryName(itemId);
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
+
+ // Disable the menu button so that the menu won't interfere with the tab close animation.
+ mMenuButton.setEnabled(false);
+ ((CloseAllPanelView) mPanelNormal).closeAll();
+ } else {
+ Log.e(LOGTAG, "Close all tabs menu item should only be visible for normal tabs panel");
+ }
+ return true;
+ }
+
+ if (itemId == R.id.close_private_tabs) {
+ if (mCurrentPanel == Panel.PRIVATE_TABS) {
+ final String extras = getResources().getResourceEntryName(itemId);
+ Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, extras);
+
+ ((CloseAllPanelView) mPanelPrivate).closeAll();
+ } else {
+ Log.e(LOGTAG, "Close private tabs menu item should only be visible for private tabs panel");
+ }
+ return true;
+ }
+
+ if (itemId == R.id.new_tab || itemId == R.id.new_private_tab) {
+ hide();
+ }
+
+ return mActivity.onOptionsItemSelected(item);
+ }
+
private static int getTabContainerHeight(TabsListContainer listContainer) {
Resources resources = listContainer.getContext().getResources();
@@ -350,12 +420,17 @@ public class TabsPanel extends LinearLayout
mFooter.setVisibility(View.GONE);
mAddTab.setVisibility(View.INVISIBLE);
+
+ mMenuButton.setVisibility(View.INVISIBLE);
} else {
if (mFooter != null)
mFooter.setVisibility(View.VISIBLE);
mAddTab.setVisibility(View.VISIBLE);
mAddTab.setImageLevel(index);
+
+ mMenuButton.setVisibility(View.VISIBLE);
+ mMenuButton.setEnabled(true);
}
if (isSideBar()) {
@@ -374,6 +449,7 @@ public class TabsPanel extends LinearLayout
if (mVisible) {
mVisible = false;
+ mPopupMenu.dismiss();
dispatchLayoutChange(0, 0);
}
}
diff --git a/mobile/android/base/tabspanel/TabsTray.java b/mobile/android/base/tabspanel/TabsTray.java
index 2f0aad781f18..18ccc9e88c27 100644
--- a/mobile/android/base/tabspanel/TabsTray.java
+++ b/mobile/android/base/tabspanel/TabsTray.java
@@ -17,6 +17,7 @@ import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.PropertyAnimator.Property;
import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.TwoWayView;
import org.mozilla.gecko.widget.TabThumbnailWrapper;
@@ -38,38 +39,44 @@ import android.widget.ImageView;
import android.widget.TextView;
class TabsTray extends TwoWayView
- implements TabsPanel.PanelView {
+ implements TabsPanel.PanelView,
+ TabsPanel.CloseAllPanelView {
private static final String LOGTAG = "Gecko" + TabsTray.class.getSimpleName();
private Context mContext;
private TabsPanel mTabsPanel;
+ final private boolean mIsPrivate;
+
private TabsAdapter mTabsAdapter;
private List mPendingClosedTabs;
- private int mCloseAnimationCount;
+ private int mCloseAnimationCount = 0;
+ private int mCloseAllAnimationCount = 0;
private TabSwipeGestureListener mSwipeListener;
// Time to animate non-flinged tabs of screen, in milliseconds
private static final int ANIMATION_DURATION = 250;
+ // Time between starting successive tab animations in closeAllTabs.
+ private static final int ANIMATION_CASCADE_DELAY = 75;
+
private int mOriginalSize = 0;
public TabsTray(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
- mCloseAnimationCount = 0;
mPendingClosedTabs = new ArrayList();
setItemsCanFocus(true);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsTray);
- boolean isPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
+ mIsPrivate = (a.getInt(R.styleable.TabsTray_tabs, 0x0) == 1);
a.recycle();
- mTabsAdapter = new TabsAdapter(mContext, isPrivate);
+ mTabsAdapter = new TabsAdapter(mContext);
setAdapter(mTabsAdapter);
mSwipeListener = new TabSwipeGestureListener();
@@ -137,15 +144,13 @@ class TabsTray extends TwoWayView
// Adapter to bind tabs into a list
private class TabsAdapter extends BaseAdapter implements Tabs.OnTabsChangedListener {
private Context mContext;
- private boolean mIsPrivate;
private ArrayList mTabs;
private LayoutInflater mInflater;
private Button.OnClickListener mOnCloseClickListener;
- public TabsAdapter(Context context, boolean isPrivate) {
+ public TabsAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(mContext);
- mIsPrivate = isPrivate;
mOnCloseClickListener = new Button.OnClickListener() {
@Override
@@ -281,16 +286,21 @@ class TabsTray extends TwoWayView
private void resetTransforms(View view) {
ViewHelper.setAlpha(view, 1);
- if (mOriginalSize == 0)
- return;
if (isVertical()) {
- ViewHelper.setHeight(view, mOriginalSize);
ViewHelper.setTranslationX(view, 0);
} else {
- ViewHelper.setWidth(view, mOriginalSize);
ViewHelper.setTranslationY(view, 0);
}
+
+ // We only need to reset the height or width after individual tab close animations.
+ if (mOriginalSize != 0) {
+ if (isVertical()) {
+ ViewHelper.setHeight(view, mOriginalSize);
+ } else {
+ ViewHelper.setWidth(view, mOriginalSize);
+ }
+ }
}
@Override
@@ -320,6 +330,75 @@ class TabsTray extends TwoWayView
return (getOrientation().compareTo(TwoWayView.Orientation.VERTICAL) == 0);
}
+ @Override
+ public void closeAll() {
+ final int childCount = getChildCount();
+
+ // Just close the panel if there are no tabs to close.
+ if (childCount == 0) {
+ autoHidePanel();
+ return;
+ }
+
+ // Disable the view so that gestures won't interfere wth the tab close animation.
+ setEnabled(false);
+
+ // Delay starting each successive animation to create a cascade effect.
+ int cascadeDelay = 0;
+
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View view = getChildAt(i);
+ final PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
+ animator.attach(view, Property.ALPHA, 0);
+
+ if (isVertical()) {
+ animator.attach(view, Property.TRANSLATION_X, view.getWidth());
+ } else {
+ animator.attach(view, Property.TRANSLATION_Y, view.getHeight());
+ }
+
+ mCloseAllAnimationCount++;
+
+ animator.addPropertyAnimationListener(new PropertyAnimator.PropertyAnimationListener() {
+ @Override
+ public void onPropertyAnimationStart() { }
+
+ @Override
+ public void onPropertyAnimationEnd() {
+ mCloseAllAnimationCount--;
+ if (mCloseAllAnimationCount > 0) {
+ return;
+ }
+
+ // Hide the panel after the animation is done.
+ autoHidePanel();
+
+ // Re-enable the view after the animation is done.
+ TabsTray.this.setEnabled(true);
+
+ // Then actually close all the tabs.
+ final Iterable tabs = Tabs.getInstance().getTabsInOrder();
+ for (Tab tab : tabs) {
+ // In the normal panel we want to close all tabs (both private and normal),
+ // but in the private panel we only want to close private tabs.
+ if (!mIsPrivate || tab.isPrivate()) {
+ Tabs.getInstance().closeTab(tab, false);
+ }
+ }
+ }
+ });
+
+ ThreadUtils.getUiHandler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ animator.start();
+ }
+ }, cascadeDelay);
+
+ cascadeDelay += ANIMATION_CASCADE_DELAY;
+ }
+ }
+
private void animateClose(final View view, int pos) {
PropertyAnimator animator = new PropertyAnimator(ANIMATION_DURATION);
animator.attach(view, Property.ALPHA, 0);
@@ -565,7 +644,7 @@ class TabsTray extends TwoWayView
}
case MotionEvent.ACTION_MOVE: {
- if (mSwipeView == null)
+ if (mSwipeView == null || mVelocityTracker == null)
break;
mVelocityTracker.addMovement(e);
diff --git a/testing/profiles/prefs_general.js b/testing/profiles/prefs_general.js
index 8cbae8b3bb48..8295d24b1e32 100644
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -200,3 +200,7 @@ user_pref('identity.fxaccounts.auth.uri', 'https://%(server)s/fxa-dummy/');
// Enable logging of APZ test data (see bug 961289).
user_pref('apz.test.logging_enabled', true);
+
+// Make sure Translation won't hit the network.
+user_pref("browser.translation.bing.authURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
+user_pref("browser.translation.bing.translateArrayURL", "http://%(server)s/browser/browser/components/translation/test/bing.sjs");
diff --git a/toolkit/devtools/apps/Simulator.jsm b/toolkit/devtools/apps/Simulator.jsm
index d430599932d1..c7f51be355a1 100644
--- a/toolkit/devtools/apps/Simulator.jsm
+++ b/toolkit/devtools/apps/Simulator.jsm
@@ -8,17 +8,24 @@ Components.utils.import("resource://gre/modules/devtools/event-emitter.js");
const EXPORTED_SYMBOLS = ["Simulator"];
+function getVersionNumber(fullVersion) {
+ return fullVersion.match(/(\d+\.\d+)/)[0];
+}
+
const Simulator = {
_simulators: {},
- register: function (version, simulator) {
- this._simulators[version] = simulator;
- this.emit("register");
+ register: function (label, simulator) {
+ // simulators register themselves as "Firefox OS X.Y"
+ let versionNumber = getVersionNumber(label);
+ this._simulators[versionNumber] = simulator;
+ this.emit("register", versionNumber);
},
- unregister: function (version) {
- delete this._simulators[version];
- this.emit("unregister");
+ unregister: function (label) {
+ let versionNumber = getVersionNumber(label);
+ delete this._simulators[versionNumber];
+ this.emit("unregister", versionNumber);
},
availableVersions: function () {