This commit is contained in:
Wes Kocher 2014-06-19 16:14:35 -07:00
Родитель 282b3eff80 8ab686ea99
Коммит 7fe3c4effc
77 изменённых файлов: 1970 добавлений и 208 удалений

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -93,10 +93,16 @@ this.Translation = {
*/
function TranslationUI(aBrowser) {
this.browser = aBrowser;
aBrowser.messageManager.addMessageListener("Translation:Finished", this);
}
TranslationUI.prototype = {
get browser() this._browser,
set browser(aBrowser) {
if (this._browser)
this._browser.messageManager.removeMessageListener("Translation:Finished", this);
aBrowser.messageManager.addMessageListener("Translation:Finished", this);
this._browser = aBrowser;
},
translate: function(aFrom, aTo) {
if (aFrom == aTo ||
(this.state == Translation.STATE_TRANSLATED &&
@ -124,7 +130,17 @@ TranslationUI.prototype = {
if (notification)
PopupNotifications.remove(notification);
let callback = aTopic => {
let callback = (aTopic, aNewBrowser) => {
if (aTopic == "swapping") {
let infoBarVisible =
this.notificationBox.getNotificationWithValue("translation");
aNewBrowser.translationUI = this;
this.browser = aNewBrowser;
if (infoBarVisible)
this.showTranslationInfoBar();
return true;
}
if (aTopic != "showing")
return false;
let notification = this.notificationBox.getNotificationWithValue("translation");

Просмотреть файл

@ -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,

Просмотреть файл

@ -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();
}
};

Просмотреть файл

@ -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]

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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];
});
}

15
browser/components/translation/test/fixtures/bug1022725-fr.html поставляемый Normal file
Просмотреть файл

@ -0,0 +1,15 @@
<!doctype html>
<html lang="fr">
<head>
<!--
- Text retrieved from http://fr.wikipedia.org/wiki/Coupe_du_monde_de_football_de_2014
- at 06/13/2014, Creative Commons Attribution-ShareAlike License.
-->
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>test</title>
</head>
<body>
<h1>Coupe du monde de football de 2014</h1>
<div>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.</div>
</body>
</html>

Просмотреть файл

@ -0,0 +1,22 @@
<ArrayOfTranslateArrayResponse xmlns="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<TranslateArrayResponse>
<From>fr</From>
<OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a:int>34</a:int>
</OriginalTextSentenceLengths>
<TranslatedText>Football's 2014 World Cup</TranslatedText>
<TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a:int>25</a:int>
</TranslatedTextSentenceLengths>
</TranslateArrayResponse>
<TranslateArrayResponse>
<From>fr</From>
<OriginalTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a:int>508</a:int>
</OriginalTextSentenceLengths>
<TranslatedText>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.</TranslatedText>
<TranslatedTextSentenceLengths xmlns:a="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<a:int>475</a:int>
</TranslatedTextSentenceLengths>
</TranslateArrayResponse>
</ArrayOfTranslateArrayResponse>

Просмотреть файл

@ -152,11 +152,6 @@
<parameter name="aTranslation"/>
<body>
<![CDATA[
if (Translation.serviceUnavailable) {
this.state = Translation.STATE_UNAVAILABLE;
return;
}
this.translation = aTranslation;
let bundle = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)

Просмотреть файл

@ -127,7 +127,7 @@
<template id="simulator-item-template">
<span>
<button class="simulator-item action-primary" onclick="UI.startSimulator(this.dataset.version)" template='{"type":"attribute","path":"version","name":"data-version"}' title="&connection.startSimulatorTooltip;">
<span template='{"type":"textContent", "path":"version"}'></span>
<span template='{"type":"textContent", "path":"label"}'></span>
</button>
</span>
</template>

Просмотреть файл

@ -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
}
});
}

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
%webideDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="chrome://webide/skin/addons.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://webide/content/addons.js"></script>
</head>
<body>
<div id="controls">
<a id="aboutaddons">&addons_aboutaddons;</a>
<a id="close">&deck_close;</a>
</div>
<h1>&addons_title;</h1>
<ul></ul>
</body>
</html>

Просмотреть файл

@ -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

Просмотреть файл

@ -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) {

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
%webideDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="chrome://webide/skin/tabledoc.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://webide/content/permissionstable.js"></script>
</head>
<body>
<div id="controls">
<a id="close">&deck_close;</a>
</div>
<h1>&permissionstable_title;</h1>
<table class="permissionstable">
<tr>
<th>&permissionstable_name_header;</th>
<th>type:web</th>
<th>type:privileged</th>
<th>type:certified</th>
</tr>
</table>
</body>
</html>

Просмотреть файл

@ -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();
}
}

Просмотреть файл

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
<!ENTITY % webideDTD SYSTEM "chrome://webide/content/webide.dtd" >
%webideDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="chrome://webide/skin/tabledoc.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://webide/content/runtimedetails.js"></script>
</head>
<body>
<div id="controls">
<a id="close">&deck_close;</a>
</div>
<h1>&runtimedetails_title;</h1>
<table></table>
</body>
</html>

Просмотреть файл

@ -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 = "<tr><th>Name</th><th>type:web</th><th>type:privileged</th><th>type:certified</th></tr>";
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");
},
}

Просмотреть файл

@ -41,6 +41,8 @@
<command id="cmd_showRuntimeDetails" oncommand="Cmds.showRuntimeDetails()" label="&runtimeMenu_showDetails_label;"/>
<command id="cmd_takeScreenshot" oncommand="Cmds.takeScreenshot()" label="&runtimeMenu_takeScreenshot_label;"/>
<command id="cmd_toggleEditor" oncommand="Cmds.toggleEditors()" label="&viewMenu_toggleEditor_label;"/>
<command id="cmd_showAddons" oncommand="Cmds.showAddons()"/>
<command id="cmd_showTroubleShooting" oncommand="Cmds.showTroubleShooting()"/>
<command id="cmd_play" oncommand="Cmds.play()"/>
<command id="cmd_stop" oncommand="Cmds.stop()"/>
<command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/>
@ -76,6 +78,7 @@
<menu id="menu-view" label="&viewMenu_label;" accesskey="&viewMenu_accesskey;">
<menupopup id="menu-ViewPopup">
<menuitem command="cmd_toggleEditor" key="key_toggleEditor" accesskey="&viewMenu_toggleEditor_accesskey;"/>
<menuitem command="cmd_showAddons" label="&viewMenu_showAddons_label;" accesskey="&viewMenu_showAddons_accesskey;"/>
</menupopup>
</menu>
@ -136,8 +139,11 @@
<panel id="runtime-panel" type="arrow" position="bottomcenter topright" consumeoutsideclicks="true" animate="false">
<vbox flex="1">
<label class="panel-header">&runtimePanel_USBDevices;</label>
<toolbarbutton class="panel-item-help" label="&runtimePanel_nousbdevice;" id="runtime-panel-nousbdevice" command="cmd_showTroubleShooting"/>
<toolbarbutton class="panel-item-help" label="&runtimePanel_noadbhelper;" id="runtime-panel-noadbhelper" command="cmd_showAddons"/>
<vbox id="runtime-panel-usbruntime"></vbox>
<label class="panel-header">&runtimePanel_simulators;</label>
<toolbarbutton class="panel-item-help" label="&runtimePanel_nosimulator;" id="runtime-panel-nosimulator" command="cmd_showAddons"/>
<vbox id="runtime-panel-simulators"></vbox>
<label class="panel-header">&runtimePanel_custom;</label>
<vbox id="runtime-panel-custom"></vbox>
@ -151,9 +157,14 @@
</popupset>
<notificationbox flex="1" id="body">
<iframe id="details" flex="1" hidden="true" src="details.xhtml"/>
<iframe id="projecteditor" flex="1" hidden="true"/>
<notificationbox flex="1" id="notificationbox">
<deck flex="1" id="deck" selectedIndex="-1">
<iframe id="deck-panel-details" flex="1" src="details.xhtml"/>
<iframe id="deck-panel-projecteditor" flex="1"/>
<iframe id="deck-panel-addons" flex="1" src="addons.xhtml"/>
<iframe id="deck-panel-permissionstable" flex="1" src="permissionstable.xhtml"/>
<iframe id="deck-panel-runtimedetails" flex="1" src="runtimedetails.xhtml"/>
</deck>
</notificationbox>
<splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>

Просмотреть файл

@ -38,6 +38,8 @@
<!ENTITY viewMenu_accesskey "V">
<!ENTITY viewMenu_toggleEditor_label "Toggle Editor">
<!ENTITY viewMenu_toggleEditor_accesskey "E">
<!ENTITY viewMenu_showAddons_label "Manage simulators">
<!ENTITY viewMenu_showAddons_accesskey "M">
<!ENTITY projectButton_label "Open App">
<!ENTITY runtimeButton_label "Select Runtime">
@ -61,6 +63,9 @@
<!ENTITY runtimePanel_USBDevices "USB Devices">
<!ENTITY runtimePanel_simulators "Simulators">
<!ENTITY runtimePanel_custom "Custom">
<!ENTITY runtimePanel_nosimulator "Install Simulator">
<!ENTITY runtimePanel_noadbhelper "Install ADB Helper">
<!ENTITY runtimePanel_nousbdevice "Can't see your device?">
<!-- Lense -->
<!ENTITY details_valid_header "valid">
@ -76,3 +81,19 @@
<!ENTITY newAppHeader "Select template">
<!ENTITY newAppLoadingTemplate "Loading templates…">
<!ENTITY newAppProjectName "Project Name:">
<!-- Decks -->
<!ENTITY deck_close "close">
<!-- Addons -->
<!ENTITY addons_title "Extra Components:">
<!ENTITY addons_aboutaddons "Open Addons Manager">
<!-- Permissions Table -->
<!ENTITY permissionstable_title "Permissions Table">
<!ENTITY permissionstable_name_header "Name">
<!-- Runtime Details -->
<!ENTITY runtimedetails_title "Runtime Info">

Просмотреть файл

@ -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

Просмотреть файл

@ -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";
}
}
},
});

Просмотреть файл

@ -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");
}

Просмотреть файл

@ -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();

Двоичные данные
browser/devtools/webide/test/addons/adbhelper-linux.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/adbhelper-linux64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/adbhelper-mac64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/adbhelper-win32.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_1_0_simulator-linux.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_1_0_simulator-linux64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_1_0_simulator-mac64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_1_0_simulator-win32.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_2_0_simulator-linux.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_2_0_simulator-linux64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_2_0_simulator-mac64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_2_0_simulator-win32.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_3_0_simulator-linux.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_3_0_simulator-linux64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_3_0_simulator-mac64.xpi Normal file

Двоичный файл не отображается.

Двоичные данные
browser/devtools/webide/test/addons/fxos_3_0_simulator-win32.xpi Normal file

Двоичный файл не отображается.

Просмотреть файл

@ -0,0 +1,4 @@
{
"stable": ["1.0", "2.0"],
"unstable": ["3.0"]
}

Просмотреть файл

@ -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]

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -0,0 +1,168 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
window.onload = function() {
SimpleTest.waitForExplicitFinish();
const {GetAvailableAddons} = require("devtools/webide/addons");
const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
const {Simulator} = Cu.import("resource://gre/modules/devtools/Simulator.jsm");
let adbAddonsInstalled = promise.defer();
Devices.on("addon-status-updated", function onUpdate1() {
Devices.off("addon-status-updated", onUpdate1);
adbAddonsInstalled.resolve();
});
function onSimulatorInstalled(version) {
let deferred = promise.defer();
Simulator.on("register", function onUpdate() {
if (Simulator.getByVersion(version)) {
Simulator.off("register", onUpdate);
nextTick().then(deferred.resolve);
}
});
return deferred.promise;
}
function installSimulatorFromUI(doc, version) {
let li = doc.querySelector('[addon="simulator-' + version + '"]');
li.querySelector(".install-button").click();
return onSimulatorInstalled(version);
}
function uninstallSimulatorFromUI(doc, version) {
let deferred = promise.defer();
Simulator.on("unregister", function onUpdate() {
nextTick().then(() => {
let li = doc.querySelector('[status="uninstalled"][addon="simulator-' + version + '"]');
if (li) {
Simulator.off("unregister", onUpdate);
deferred.resolve();
} else {
deferred.reject("Can't find item");
}
})
});
let li = doc.querySelector('[status="installed"][addon="simulator-' + version + '"]');
li.querySelector(".uninstall-button").click();
return deferred.promise;
}
function uninstallADBFromUI(doc) {
let deferred = promise.defer();
Devices.on("addon-status-updated", function onUpdate() {
nextTick().then(() => {
let li = doc.querySelector('[status="uninstalled"][addon="adb"]');
if (li) {
Devices.off("addon-status-updated", onUpdate);
deferred.resolve();
} else {
deferred.reject("Can't find item");
}
})
});
let li = doc.querySelector('[status="installed"][addon="adb"]');
li.querySelector(".uninstall-button").click();
return deferred.promise;
}
Task.spawn(function* () {
ok(!Devices.helperAddonInstalled, "Helper not installed");
let win = yield openWebIDE(true);
yield adbAddonsInstalled.promise;
ok(Devices.helperAddonInstalled, "Helper has been auto-installed");
yield nextTick();
let addons = yield GetAvailableAddons();
is(addons.simulators.length, 3, "3 simulator addons to install");
let sim10 = addons.simulators.filter(a => a.version == "1.0")[0];
sim10.install();
yield onSimulatorInstalled("1.0");
win.Cmds.showAddons();
let frame = win.document.querySelector("#deck-panel-addons");
let addonDoc = frame.contentWindow.document;
let lis;
lis = addonDoc.querySelectorAll("li");
is(lis.length, 4, "4 addons listed");
lis = addonDoc.querySelectorAll('li[status="installed"]');
is(lis.length, 2, "2 addons installed");
lis = addonDoc.querySelectorAll('li[status="uninstalled"]');
is(lis.length, 2, "2 addons uninstalled");
info("Uninstalling Simulator 2.0");
yield installSimulatorFromUI(addonDoc, "2.0");
info("Uninstalling Simulator 3.0");
yield installSimulatorFromUI(addonDoc, "3.0");
yield nextTick();
let panelNode = win.document.querySelector("#runtime-panel");
let items;
items = panelNode.querySelectorAll(".runtime-panel-item-usb");
is(items.length, 1, "Found one runtime button");
items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
is(items.length, 3, "Found 3 simulators button");
yield uninstallSimulatorFromUI(addonDoc, "1.0");
yield uninstallSimulatorFromUI(addonDoc, "2.0");
yield uninstallSimulatorFromUI(addonDoc, "3.0");
items = panelNode.querySelectorAll(".runtime-panel-item-simulator");
is(items.length, 0, "No simulator listed");
let w = addonDoc.querySelector(".warning");
let display = addonDoc.defaultView.getComputedStyle(w).display
is(display, "none", "Warning about missing ADB hidden");
yield uninstallADBFromUI(addonDoc, "adb");
items = panelNode.querySelectorAll(".runtime-panel-item-usb");
is(items.length, 0, "No usb runtime listed");
display = addonDoc.defaultView.getComputedStyle(w).display
is(display, "block", "Warning about missing ADB present");
yield closeWebIDE(win);
SimpleTest.finish();
});
}
</script>
</body>
</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();

Просмотреть файл

@ -0,0 +1,126 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
let win = yield openWebIDE();
let permIframe = win.document.querySelector("#deck-panel-permissionstable");
let infoIframe = win.document.querySelector("#deck-panel-runtimedetails");
yield documentIsLoaded(permIframe.contentWindow.document);
yield documentIsLoaded(infoIframe.contentWindow.document);
win.AppManager.update("runtimelist");
let panelNode = win.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-custom");
is(items.length, 2, "Found 2 custom runtimes button");
let deferred = promise.defer();
win.AppManager.on("app-manager-update", function onUpdate(e,w) {
if (w == "list-tabs-response") {
win.AppManager.off("app-manager-update", onUpdate);
deferred.resolve();
}
});
items[1].click();
yield deferred.promise;
yield nextTick();
let perm = win.document.querySelector("#cmd_showPermissionsTable");
let info = win.document.querySelector("#cmd_showRuntimeDetails");
ok(!perm.hasAttribute("disabled"), "perm cmd enabled");
ok(!info.hasAttribute("disabled"), "info cmd enabled");
let deck = win.document.querySelector("#deck");
win.Cmds.showRuntimeDetails();
is(deck.selectedPanel, infoIframe, "info iframe selected");
yield infoIframe.contentWindow.getRawPermissionsTablePromise;
yield nextTick();
// device info and permissions content is checked in other tests
// We just test one value to make sure we get something
let doc = infoIframe.contentWindow.document;
let trs = doc.querySelectorAll("tr");
let found = false;
for (let tr of trs) {
let [name,val] = tr.querySelectorAll("td");
if (name.textContent == "appid") {
found = true;
is(val.textContent, Services.appinfo.ID, "appid has the right value");
}
}
ok(found, "Found appid line");
win.Cmds.showPermissionsTable();
is(deck.selectedPanel, permIframe, "permission iframe selected");
yield infoIframe.contentWindow.getDescriptionPromise;
yield nextTick();
doc = permIframe.contentWindow.document;
trs = doc.querySelectorAll(".line");
found = false;
for (let tr of trs) {
let [name,v1,v2,v3] = tr.querySelectorAll("td");
if (name.textContent == "geolocation") {
found = true;
is(v1.className, "permprompt", "geolocation perm is valid");
is(v2.className, "permprompt", "geolocation perm is valid");
is(v3.className, "permprompt", "geolocation perm is valid");
}
}
ok(found, "Found geolocation line");
doc.querySelector("#close").click();
ok(!deck.selectedPanel, "No panel selected");
DebuggerServer.destroy();
yield closeWebIDE(win);
SimpleTest.finish();
}).then(null, e => {
ok(false, "Exception: " + e);
SimpleTest.finish();
});
}
</script>
</body>
</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();

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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)

Просмотреть файл

@ -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);
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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");

Просмотреть файл

@ -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;
}

Просмотреть файл

@ -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)

Двоичные данные
browser/themes/linux/tabbrowser/tab-arrow-left-inverted.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 250 B

Двоичные данные
browser/themes/linux/tabbrowser/tab-arrow-left.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 368 B

Просмотреть файл

@ -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)

Просмотреть файл

@ -256,6 +256,7 @@ size. -->
<!ENTITY new_tab "New Tab">
<!ENTITY new_private_tab "New Private Tab">
<!ENTITY close_all_tabs "Close All Tabs">
<!ENTITY close_private_tabs "Close Private Tabs">
<!ENTITY tabs_normal "Tabs">
<!ENTITY tabs_private "Private">
<!ENTITY tabs_synced "Synced">

Просмотреть файл

@ -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);
}
}

Двоичные данные
mobile/android/base/resources/drawable-hdpi/menu_tabs.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 136 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 136 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 136 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 138 B

Двоичные данные
mobile/android/base/resources/drawable-mdpi/menu_tabs.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 136 B

Двоичные данные
mobile/android/base/resources/drawable-xhdpi/menu_tabs.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 138 B

Просмотреть файл

@ -6,12 +6,24 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ImageButton android:id="@+id/add_tab"
android:layout_width="fill_parent"
android:layout_width="@dimen/browser_toolbar_height"
android:layout_height="@dimen/browser_toolbar_height"
android:padding="14dip"
android:src="@drawable/tab_new_level"
android:contentDescription="@string/new_tab"
android:background="@drawable/action_bar_button_inverse"
android:gravity="center"/>
android:background="@drawable/action_bar_button_inverse"/>
<View android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1.0"/>
<ImageButton android:id="@+id/menu"
style="@style/UrlBar.ImageButton"
android:layout_width="@dimen/browser_toolbar_height"
android:layout_height="@dimen/browser_toolbar_height"
android:padding="@dimen/browser_toolbar_button_padding"
android:src="@drawable/menu_tabs"
android:contentDescription="@string/menu"
android:background="@drawable/action_bar_button"/>
</merge>

Просмотреть файл

@ -25,4 +25,13 @@
android:contentDescription="@string/new_tab"
android:background="@drawable/action_bar_button_inverse"/>
<ImageButton android:id="@+id/menu"
style="@style/UrlBar.ImageButton"
android:layout_width="@dimen/browser_toolbar_height"
android:layout_height="@dimen/browser_toolbar_height"
android:padding="@dimen/browser_toolbar_button_padding"
android:src="@drawable/menu_tabs"
android:contentDescription="@string/menu"
android:background="@drawable/action_bar_button"/>
</merge>

Просмотреть файл

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/new_tab"
android:title="@string/new_tab"/>
<item android:id="@+id/new_private_tab"
android:title="@string/new_private_tab"/>
<item android:id="@+id/close_all_tabs"
android:title="@string/close_all_tabs"/>
<item android:id="@+id/close_private_tabs"
android:title="@string/close_private_tabs"/>
</menu>

Просмотреть файл

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/new_tab"
android:title="@string/new_tab"/>
<item android:id="@+id/new_private_tab"
android:title="@string/new_private_tab"/>
<item android:id="@+id/close_all_tabs"
android:title="@string/close_all_tabs"/>
<item android:id="@+id/close_private_tabs"
android:title="@string/close_private_tabs"/>
</menu>

Просмотреть файл

@ -246,6 +246,7 @@
<string name="new_tab">&new_tab;</string>
<string name="new_private_tab">&new_private_tab;</string>
<string name="close_all_tabs">&close_all_tabs;</string>
<string name="close_private_tabs">&close_private_tabs;</string>
<string name="tabs_normal">&tabs_normal;</string>
<string name="tabs_private">&tabs_private;</string>
<string name="tabs_synced">&tabs_synced;</string>

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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<View> 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<View>();
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<Tab> 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<Tab> 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);

Просмотреть файл

@ -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");

Просмотреть файл

@ -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 () {