diff options
Diffstat (limited to 'devtools/client/webide/content')
34 files changed, 4366 insertions, 0 deletions
diff --git a/devtools/client/webide/content/addons.js b/devtools/client/webide/content/addons.js new file mode 100644 index 000000000..3948b040f --- /dev/null +++ b/devtools/client/webide/content/addons.js @@ -0,0 +1,135 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {GetAvailableAddons, ForgetAddonsList} = require("devtools/client/webide/modules/addons"); +const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.querySelector("#aboutaddons").onclick = function () { + let browserWin = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); + if (browserWin && browserWin.BrowserOpenAddonsMgr) { + browserWin.BrowserOpenAddonsMgr("addons://list/extension"); + } + }; + document.querySelector("#close").onclick = CloseUI; + GetAvailableAddons().then(BuildUI, (e) => { + console.error(e); + window.alert(Strings.formatStringFromName("error_cantFetchAddonsJSON", [e], 1)); + }); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + ForgetAddonsList(); +}, true); + +function CloseUI() { + window.parent.UI.openProject(); +} + +function BuildUI(addons) { + BuildItem(addons.adb, "adb"); + BuildItem(addons.adapters, "adapters"); + for (let addon of addons.simulators) { + BuildItem(addon, "simulator"); + } +} + +function BuildItem(addon, type) { + + 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": + window.parent.UI.reportError("error_operationFail", 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); + + let name = document.createElement("span"); + name.className = "name"; + + switch (type) { + case "adb": + li.setAttribute("addon", type); + name.textContent = Strings.GetStringFromName("addons_adb_label"); + break; + case "adapters": + li.setAttribute("addon", type); + try { + name.textContent = Strings.GetStringFromName("addons_adapters_label"); + } catch (e) { + // This code (bug 1081093) will be backported to Aurora, which doesn't + // contain this string. + name.textContent = "Tools Adapters Add-on"; + } + break; + case "simulator": + li.setAttribute("addon", "simulator-" + addon.version); + let stability = Strings.GetStringFromName("addons_" + addon.stability); + name.textContent = Strings.formatStringFromName("addons_simulator_label", [addon.version, stability], 2); + break; + } + + 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 (type == "adb") { + 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/devtools/client/webide/content/addons.xhtml b/devtools/client/webide/content/addons.xhtml new file mode 100644 index 000000000..6f3bc1e7c --- /dev/null +++ b/devtools/client/webide/content/addons.xhtml @@ -0,0 +1,31 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <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> diff --git a/devtools/client/webide/content/details.js b/devtools/client/webide/content/details.js new file mode 100644 index 000000000..9097cd8c5 --- /dev/null +++ b/devtools/client/webide/content/details.js @@ -0,0 +1,139 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {ProjectBuilding} = require("devtools/client/webide/modules/build"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.addEventListener("visibilitychange", updateUI, true); + AppManager.on("app-manager-update", onAppManagerUpdate); + updateUI(); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + AppManager.off("app-manager-update", onAppManagerUpdate); +}, true); + +function onAppManagerUpdate(event, what, details) { + if (what == "project" || + what == "project-validated") { + updateUI(); + } +} + +function resetUI() { + document.querySelector("#toolbar").classList.add("hidden"); + document.querySelector("#type").classList.add("hidden"); + document.querySelector("#descriptionHeader").classList.add("hidden"); + document.querySelector("#manifestURLHeader").classList.add("hidden"); + document.querySelector("#locationHeader").classList.add("hidden"); + + document.body.className = ""; + document.querySelector("#icon").src = ""; + document.querySelector("h1").textContent = ""; + document.querySelector("#description").textContent = ""; + document.querySelector("#type").textContent = ""; + document.querySelector("#manifestURL").textContent = ""; + document.querySelector("#location").textContent = ""; + + document.querySelector("#prePackageLog").hidden = true; + + document.querySelector("#errorslist").innerHTML = ""; + document.querySelector("#warningslist").innerHTML = ""; + +} + +function updateUI() { + resetUI(); + + let project = AppManager.selectedProject; + if (!project) { + return; + } + + if (project.type != "runtimeApp" && project.type != "mainProcess") { + document.querySelector("#toolbar").classList.remove("hidden"); + document.querySelector("#locationHeader").classList.remove("hidden"); + document.querySelector("#location").textContent = project.location; + } + + document.body.className = project.validationStatus; + document.querySelector("#icon").src = project.icon; + document.querySelector("h1").textContent = project.name; + + let manifest; + if (project.type == "runtimeApp") { + manifest = project.app.manifest; + } else { + manifest = project.manifest; + } + + if (manifest) { + if (manifest.description) { + document.querySelector("#descriptionHeader").classList.remove("hidden"); + document.querySelector("#description").textContent = manifest.description; + } + + document.querySelector("#type").classList.remove("hidden"); + + if (project.type == "runtimeApp") { + let manifestURL = AppManager.getProjectManifestURL(project); + document.querySelector("#type").textContent = manifest.type || "web"; + document.querySelector("#manifestURLHeader").classList.remove("hidden"); + document.querySelector("#manifestURL").textContent = manifestURL; + } else if (project.type == "mainProcess") { + document.querySelector("#type").textContent = project.name; + } else { + document.querySelector("#type").textContent = project.type + " " + (manifest.type || "web"); + } + + if (project.type == "packaged") { + let manifestURL = AppManager.getProjectManifestURL(project); + if (manifestURL) { + document.querySelector("#manifestURLHeader").classList.remove("hidden"); + document.querySelector("#manifestURL").textContent = manifestURL; + } + } + } + + if (project.type != "runtimeApp" && project.type != "mainProcess") { + ProjectBuilding.hasPrepackage(project).then(hasPrepackage => { + document.querySelector("#prePackageLog").hidden = !hasPrepackage; + }); + } + + let errorsNode = document.querySelector("#errorslist"); + let warningsNode = document.querySelector("#warningslist"); + + if (project.errors) { + for (let e of project.errors) { + let li = document.createElement("li"); + li.textContent = e; + errorsNode.appendChild(li); + } + } + + if (project.warnings) { + for (let w of project.warnings) { + let li = document.createElement("li"); + li.textContent = w; + warningsNode.appendChild(li); + } + } + + AppManager.update("details"); +} + +function showPrepackageLog() { + window.top.UI.selectDeckPanel("logs"); +} + +function removeProject() { + AppManager.removeSelectedProject(); +} diff --git a/devtools/client/webide/content/details.xhtml b/devtools/client/webide/content/details.xhtml new file mode 100644 index 000000000..a04c37b0c --- /dev/null +++ b/devtools/client/webide/content/details.xhtml @@ -0,0 +1,54 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/details.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/details.js"></script> + </head> + <body> + + <div id="toolbar"> + <button onclick="removeProject()">&details_removeProject_button;</button> + <p id="validation_status"> + <span class="valid">&details_valid_header;</span> + <span class="warning">&details_warning_header;</span> + <span class="error">&details_error_header;</span> + </p> + </div> + + <header> + <img id="icon"></img> + <div> + <h1></h1> + <p id="type"></p> + </div> + </header> + + <main> + <h3 id="descriptionHeader">&details_description;</h3> + <p id="description"></p> + + <h3 id="locationHeader">&details_location;</h3> + <p id="location"></p> + + <h3 id="manifestURLHeader">&details_manifestURL;</h3> + <p id="manifestURL"></p> + + <button id="prePackageLog" onclick="showPrepackageLog()" hidden="true">&details_showPrepackageLog_button;</button> + </main> + + <ul class="validation_messages" id="errorslist"></ul> + <ul class="validation_messages" id="warningslist"></ul> + + </body> +</html> diff --git a/devtools/client/webide/content/devicepreferences.js b/devtools/client/webide/content/devicepreferences.js new file mode 100644 index 000000000..14c020f12 --- /dev/null +++ b/devtools/client/webide/content/devicepreferences.js @@ -0,0 +1,81 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {Connection} = require("devtools/shared/client/connection-manager"); +const ConfigView = require("devtools/client/webide/modules/config-view"); + +var configView = new ConfigView(window); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + AppManager.on("app-manager-update", OnAppManagerUpdate); + document.getElementById("close").onclick = CloseUI; + document.getElementById("device-fields").onchange = UpdateField; + document.getElementById("device-fields").onclick = CheckReset; + document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField; + document.getElementById("custom-value").onclick = UpdateNewField; + document.getElementById("custom-value-type").onchange = ClearNewFields; + document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit; + 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 == "runtime-global-actors") { + BuildUI(); + } +} + +function CheckNewFieldSubmit(event) { + configView.checkNewFieldSubmit(event); +} + +function UpdateNewField() { + configView.updateNewField(); +} + +function ClearNewFields() { + configView.clearNewFields(); +} + +function CheckReset(event) { + configView.checkReset(event); +} + +function UpdateField(event) { + configView.updateField(event); +} + +function SearchField(event) { + configView.search(event); +} + +var getAllPrefs; // Used by tests +function BuildUI() { + configView.resetTable(); + + if (AppManager.connection && + AppManager.connection.status == Connection.Status.CONNECTED && + AppManager.preferenceFront) { + configView.front = AppManager.preferenceFront; + configView.kind = "Pref"; + configView.includeTypeName = true; + + getAllPrefs = AppManager.preferenceFront.getAllPrefs() + .then(json => configView.generateDisplay(json)); + } else { + CloseUI(); + } +} diff --git a/devtools/client/webide/content/devicepreferences.xhtml b/devtools/client/webide/content/devicepreferences.xhtml new file mode 100644 index 000000000..dafb6f15f --- /dev/null +++ b/devtools/client/webide/content/devicepreferences.xhtml @@ -0,0 +1,49 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/config-view.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/devicepreferences.js"></script> + </head> + <body> + <header> + <div id="controls"> + <a id="close">&deck_close;</a> + </div> + <h1>&devicepreference_title;</h1> + <div id="search"> + <input type="text" id="search-bar" placeholder="&devicepreference_search;"/> + </div> + </header> + <table id="device-fields"> + <tr id="add-custom-field"> + <td> + <select id="custom-value-type"> + <option value="" selected="selected">&device_typenone;</option> + <option value="boolean">&device_typeboolean;</option> + <option value="number">&device_typenumber;</option> + <option value="string">&device_typestring;</option> + </select> + <input type="text" id="custom-value-name" placeholder="&devicepreference_newname;"/> + </td> + <td class="custom-input"> + <input type="text" id="custom-value-text" placeholder="&devicepreference_newtext;"/> + </td> + <td> + <button id="custom-value" class="new-editable">&devicepreference_addnew;</button> + </td> + </tr> + </table> + </body> +</html> diff --git a/devtools/client/webide/content/devicesettings.js b/devtools/client/webide/content/devicesettings.js new file mode 100644 index 000000000..987df5995 --- /dev/null +++ b/devtools/client/webide/content/devicesettings.js @@ -0,0 +1,81 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {Connection} = require("devtools/shared/client/connection-manager"); +const ConfigView = require("devtools/client/webide/modules/config-view"); + +var configView = new ConfigView(window); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + AppManager.on("app-manager-update", OnAppManagerUpdate); + document.getElementById("close").onclick = CloseUI; + document.getElementById("device-fields").onchange = UpdateField; + document.getElementById("device-fields").onclick = CheckReset; + document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchField; + document.getElementById("custom-value").onclick = UpdateNewField; + document.getElementById("custom-value-type").onchange = ClearNewFields; + document.getElementById("add-custom-field").onkeyup = CheckNewFieldSubmit; + 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 == "runtime-global-actors") { + BuildUI(); + } +} + +function CheckNewFieldSubmit(event) { + configView.checkNewFieldSubmit(event); +} + +function UpdateNewField() { + configView.updateNewField(); +} + +function ClearNewFields() { + configView.clearNewFields(); +} + +function CheckReset(event) { + configView.checkReset(event); +} + +function UpdateField(event) { + configView.updateField(event); +} + +function SearchField(event) { + configView.search(event); +} + +var getAllSettings; // Used by tests +function BuildUI() { + configView.resetTable(); + + if (AppManager.connection && + AppManager.connection.status == Connection.Status.CONNECTED && + AppManager.settingsFront) { + configView.front = AppManager.settingsFront; + configView.kind = "Setting"; + configView.includeTypeName = false; + + getAllSettings = AppManager.settingsFront.getAllSettings() + .then(json => configView.generateDisplay(json)); + } else { + CloseUI(); + } +} diff --git a/devtools/client/webide/content/devicesettings.xhtml b/devtools/client/webide/content/devicesettings.xhtml new file mode 100644 index 000000000..0406c6f07 --- /dev/null +++ b/devtools/client/webide/content/devicesettings.xhtml @@ -0,0 +1,50 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/config-view.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/devicesettings.js"></script> + </head> + <body> + <header> + <div id="controls"> + <a id="close">&deck_close;</a> + </div> + <h1>&devicesetting_title;</h1> + <div id="search"> + <input type="text" id="search-bar" placeholder="&devicesetting_search;"/> + </div> + </header> + <table id="device-fields"> + <tr id="add-custom-field"> + <td> + <select id="custom-value-type"> + <option value="" selected="selected">&device_typenone;</option> + <option value="boolean">&device_typeboolean;</option> + <option value="number">&device_typenumber;</option> + <option value="string">&device_typestring;</option> + <option value="object">&device_typeobject;</option> + </select> + <input type="text" id="custom-value-name" placeholder="&devicesetting_newname;"/> + </td> + <td class="custom-input"> + <input type="text" id="custom-value-text" placeholder="&devicesetting_newtext;"/> + </td> + <td> + <button id="custom-value" class="new-editable">&devicesetting_addnew;</button> + </td> + </tr> + </table> + </body> +</html> diff --git a/devtools/client/webide/content/jar.mn b/devtools/client/webide/content/jar.mn new file mode 100644 index 000000000..db79fdb51 --- /dev/null +++ b/devtools/client/webide/content/jar.mn @@ -0,0 +1,38 @@ +# 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/. + +webide.jar: +% content webide %content/ + content/webide.xul (webide.xul) + content/webide.js (webide.js) + content/newapp.xul (newapp.xul) + content/newapp.js (newapp.js) + content/details.xhtml (details.xhtml) + content/details.js (details.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) + content/prefs.js (prefs.js) + content/prefs.xhtml (prefs.xhtml) + content/monitor.xhtml (monitor.xhtml) + content/monitor.js (monitor.js) + content/devicepreferences.js (devicepreferences.js) + content/devicepreferences.xhtml (devicepreferences.xhtml) + content/devicesettings.js (devicesettings.js) + content/devicesettings.xhtml (devicesettings.xhtml) + content/wifi-auth.js (wifi-auth.js) + content/wifi-auth.xhtml (wifi-auth.xhtml) + content/logs.xhtml (logs.xhtml) + content/logs.js (logs.js) + content/project-listing.xhtml (project-listing.xhtml) + content/project-listing.js (project-listing.js) + content/project-panel.js (project-panel.js) + content/runtime-panel.js (runtime-panel.js) + content/runtime-listing.xhtml (runtime-listing.xhtml) + content/runtime-listing.js (runtime-listing.js) + content/simulator.js (simulator.js) + content/simulator.xhtml (simulator.xhtml) diff --git a/devtools/client/webide/content/logs.js b/devtools/client/webide/content/logs.js new file mode 100644 index 000000000..157d83b67 --- /dev/null +++ b/devtools/client/webide/content/logs.js @@ -0,0 +1,70 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + + Logs.init(); +}); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + + Logs.uninit(); +}); + +const Logs = { + init: function () { + this.list = document.getElementById("logs"); + + Logs.onAppManagerUpdate = Logs.onAppManagerUpdate.bind(this); + AppManager.on("app-manager-update", Logs.onAppManagerUpdate); + + document.getElementById("close").onclick = Logs.close.bind(this); + }, + + uninit: function () { + AppManager.off("app-manager-update", Logs.onAppManagerUpdate); + }, + + onAppManagerUpdate: function (event, what, details) { + switch (what) { + case "pre-package": + this.prePackageLog(details); + break; + } + }, + + close: function () { + window.parent.UI.openProject(); + }, + + prePackageLog: function (msg, details) { + if (msg == "start") { + this.clear(); + } else if (msg == "succeed") { + setTimeout(function () { + Logs.close(); + }, 1000); + } else if (msg == "failed") { + this.log(details); + } else { + this.log(msg); + } + }, + + clear: function () { + this.list.innerHTML = ""; + }, + + log: function (msg) { + let line = document.createElement("li"); + line.textContent = msg; + this.list.appendChild(line); + } +}; diff --git a/devtools/client/webide/content/logs.xhtml b/devtools/client/webide/content/logs.xhtml new file mode 100644 index 000000000..8d003e509 --- /dev/null +++ b/devtools/client/webide/content/logs.xhtml @@ -0,0 +1,33 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="resource://devtools/client/themes/common.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/logs.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://devtools/content/shared/theme-switching.js"></script> + <script type="application/javascript;version=1.8" src="logs.js"></script> + </head> + <body> + + <div id="controls"> + <a id="close">&deck_close;</a> + </div> + + <h1>&logs_title;</h1> + + <ul id="logs" class="devtools-monospace"> + </ul> + + </body> +</html> diff --git a/devtools/client/webide/content/monitor.js b/devtools/client/webide/content/monitor.js new file mode 100644 index 000000000..a5d80d460 --- /dev/null +++ b/devtools/client/webide/content/monitor.js @@ -0,0 +1,741 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {AppActorFront} = require("devtools/shared/apps/app-actor-front"); +const {Connection} = require("devtools/shared/client/connection-manager"); +const EventEmitter = require("devtools/shared/event-emitter"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + window.addEventListener("resize", Monitor.resize); + window.addEventListener("unload", Monitor.unload); + + document.querySelector("#close").onclick = () => { + window.parent.UI.openProject(); + }; + + Monitor.load(); +}); + + +/** + * The Monitor is a WebIDE tool used to display any kind of time-based data in + * the form of graphs. + * + * The data can come from a Firefox OS device, simulator, or from a WebSockets + * server running locally. + * + * The format of a data update is typically an object like: + * + * { graph: 'mygraph', curve: 'mycurve', value: 42, time: 1234 } + * + * or an array of such objects. For more details on the data format, see the + * `Graph.update(data)` method. + */ +var Monitor = { + + apps: new Map(), + graphs: new Map(), + front: null, + socket: null, + wstimeout: null, + b2ginfo: false, + b2gtimeout: null, + + /** + * Add new data to the graphs, create a new graph if necessary. + */ + update: function (data, fallback) { + if (Array.isArray(data)) { + data.forEach(d => Monitor.update(d, fallback)); + return; + } + + if (Monitor.b2ginfo && data.graph === "USS") { + // If we're polling b2g-info, ignore USS updates from the device's + // USSAgents (see Monitor.pollB2GInfo()). + return; + } + + if (fallback) { + for (let key in fallback) { + if (!data[key]) { + data[key] = fallback[key]; + } + } + } + + let graph = Monitor.graphs.get(data.graph); + if (!graph) { + let element = document.createElement("div"); + element.classList.add("graph"); + document.body.appendChild(element); + + graph = new Graph(data.graph, element); + Monitor.resize(); // a scrollbar might have dis/reappeared + Monitor.graphs.set(data.graph, graph); + } + graph.update(data); + }, + + /** + * Initialize the Monitor. + */ + load: function () { + AppManager.on("app-manager-update", Monitor.onAppManagerUpdate); + Monitor.connectToRuntime(); + Monitor.connectToWebSocket(); + }, + + /** + * Clean up the Monitor. + */ + unload: function () { + AppManager.off("app-manager-update", Monitor.onAppManagerUpdate); + Monitor.disconnectFromRuntime(); + Monitor.disconnectFromWebSocket(); + }, + + /** + * Resize all the graphs. + */ + resize: function () { + for (let graph of Monitor.graphs.values()) { + graph.resize(); + } + }, + + /** + * When WebIDE connects to a new runtime, start its data forwarders. + */ + onAppManagerUpdate: function (event, what, details) { + switch (what) { + case "runtime-global-actors": + Monitor.connectToRuntime(); + break; + case "connection": + if (AppManager.connection.status == Connection.Status.DISCONNECTED) { + Monitor.disconnectFromRuntime(); + } + break; + } + }, + + /** + * Use an AppActorFront on a runtime to watch track its apps. + */ + connectToRuntime: function () { + Monitor.pollB2GInfo(); + let client = AppManager.connection && AppManager.connection.client; + let resp = AppManager._listTabsResponse; + if (client && resp && !Monitor.front) { + Monitor.front = new AppActorFront(client, resp); + Monitor.front.watchApps(Monitor.onRuntimeAppEvent); + } + }, + + /** + * Destroy our AppActorFront. + */ + disconnectFromRuntime: function () { + Monitor.unpollB2GInfo(); + if (Monitor.front) { + Monitor.front.unwatchApps(Monitor.onRuntimeAppEvent); + Monitor.front = null; + } + }, + + /** + * Try connecting to a local websockets server and accept updates from it. + */ + connectToWebSocket: function () { + let webSocketURL = Services.prefs.getCharPref("devtools.webide.monitorWebSocketURL"); + try { + Monitor.socket = new WebSocket(webSocketURL); + Monitor.socket.onmessage = function (event) { + Monitor.update(JSON.parse(event.data)); + }; + Monitor.socket.onclose = function () { + Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000); + }; + } catch (e) { + Monitor.wstimeout = setTimeout(Monitor.connectToWebsocket, 1000); + } + }, + + /** + * Used when cleaning up. + */ + disconnectFromWebSocket: function () { + clearTimeout(Monitor.wstimeout); + if (Monitor.socket) { + Monitor.socket.onclose = () => {}; + Monitor.socket.close(); + } + }, + + /** + * When an app starts on the runtime, start a monitor actor for its process. + */ + onRuntimeAppEvent: function (type, app) { + if (type !== "appOpen" && type !== "appClose") { + return; + } + + let client = AppManager.connection.client; + app.getForm().then(form => { + if (type === "appOpen") { + app.monitorClient = new MonitorClient(client, form); + app.monitorClient.start(); + app.monitorClient.on("update", Monitor.onRuntimeUpdate); + Monitor.apps.set(form.monitorActor, app); + } else { + let app = Monitor.apps.get(form.monitorActor); + if (app) { + app.monitorClient.stop(() => app.monitorClient.destroy()); + Monitor.apps.delete(form.monitorActor); + } + } + }); + }, + + /** + * Accept data updates from the monitor actors of a runtime. + */ + onRuntimeUpdate: function (type, packet) { + let fallback = {}, app = Monitor.apps.get(packet.from); + if (app) { + fallback.curve = app.manifest.name; + } + Monitor.update(packet.data, fallback); + }, + + /** + * Bug 1047355: If possible, parsing the output of `b2g-info` has several + * benefits over bug 1037465's multi-process USSAgent approach, notably: + * - Works for older Firefox OS devices (pre-2.1), + * - Doesn't need certified-apps debugging, + * - Polling time is synchronized for all processes. + * TODO: After bug 1043324 lands, consider removing this hack. + */ + pollB2GInfo: function () { + if (AppManager.selectedRuntime) { + let device = AppManager.selectedRuntime.device; + if (device && device.shell) { + device.shell("b2g-info").then(s => { + let lines = s.split("\n"); + let line = ""; + + // Find the header row to locate NAME and USS, looks like: + // ' NAME PID NICE USS PSS RSS VSIZE OOM_ADJ USER '. + while (line.indexOf("NAME") < 0) { + if (lines.length < 1) { + // Something is wrong with this output, don't trust b2g-info. + Monitor.unpollB2GInfo(); + return; + } + line = lines.shift(); + } + let namelength = line.indexOf("NAME") + "NAME".length; + let ussindex = line.slice(namelength).split(/\s+/).indexOf("USS"); + + // Get the NAME and USS in each following line, looks like: + // 'Homescreen 375 18 12.6 16.3 27.1 67.8 4 app_375'. + while (lines.length > 0 && lines[0].length > namelength) { + line = lines.shift(); + let name = line.slice(0, namelength); + let uss = line.slice(namelength).split(/\s+/)[ussindex]; + Monitor.update({ + curve: name.trim(), + value: 1024 * 1024 * parseFloat(uss) // Convert MB to bytes. + }, { + // Note: We use the fallback object to set the graph name to 'USS' + // so that Monitor.update() can ignore USSAgent updates. + graph: "USS" + }); + } + }); + } + } + Monitor.b2ginfo = true; + Monitor.b2gtimeout = setTimeout(Monitor.pollB2GInfo, 350); + }, + + /** + * Polling b2g-info doesn't work or is no longer needed. + */ + unpollB2GInfo: function () { + clearTimeout(Monitor.b2gtimeout); + Monitor.b2ginfo = false; + } + +}; + + +/** + * A MonitorClient is used as an actor client of a runtime's monitor actors, + * receiving its updates. + */ +function MonitorClient(client, form) { + this.client = client; + this.actor = form.monitorActor; + this.events = ["update"]; + + EventEmitter.decorate(this); + this.client.registerClient(this); +} +MonitorClient.prototype.destroy = function () { + this.client.unregisterClient(this); +}; +MonitorClient.prototype.start = function () { + this.client.request({ + to: this.actor, + type: "start" + }); +}; +MonitorClient.prototype.stop = function (callback) { + this.client.request({ + to: this.actor, + type: "stop" + }, callback); +}; + + +/** + * A Graph populates a container DOM element with an SVG graph and a legend. + */ +function Graph(name, element) { + this.name = name; + this.element = element; + this.curves = new Map(); + this.events = new Map(); + this.ignored = new Set(); + this.enabled = true; + this.request = null; + + this.x = d3.time.scale(); + this.y = d3.scale.linear(); + + this.xaxis = d3.svg.axis().scale(this.x).orient("bottom"); + this.yaxis = d3.svg.axis().scale(this.y).orient("left"); + + this.xformat = d3.time.format("%I:%M:%S"); + this.yformat = this.formatter(1); + this.yaxis.tickFormat(this.formatter(0)); + + this.line = d3.svg.line().interpolate("linear") + .x(function (d) { return this.x(d.time); }) + .y(function (d) { return this.y(d.value); }); + + this.color = d3.scale.category10(); + + this.svg = d3.select(element).append("svg").append("g") + .attr("transform", "translate(" + this.margin.left + "," + this.margin.top + ")"); + + this.xelement = this.svg.append("g").attr("class", "x axis").call(this.xaxis); + this.yelement = this.svg.append("g").attr("class", "y axis").call(this.yaxis); + + // RULERS on axes + let xruler = this.xruler = this.svg.select(".x.axis").append("g").attr("class", "x ruler"); + xruler.append("line").attr("y2", 6); + xruler.append("line").attr("stroke-dasharray", "1,1"); + xruler.append("text").attr("y", 9).attr("dy", ".71em"); + + let yruler = this.yruler = this.svg.select(".y.axis").append("g").attr("class", "y ruler"); + yruler.append("line").attr("x2", -6); + yruler.append("line").attr("stroke-dasharray", "1,1"); + yruler.append("text").attr("x", -9).attr("dy", ".32em"); + + let self = this; + + d3.select(element).select("svg") + .on("mousemove", function () { + let mouse = d3.mouse(this); + self.mousex = mouse[0] - self.margin.left, + self.mousey = mouse[1] - self.margin.top; + + xruler.attr("transform", "translate(" + self.mousex + ",0)"); + yruler.attr("transform", "translate(0," + self.mousey + ")"); + }); + /* .on('mouseout', function() { + self.xruler.attr('transform', 'translate(-500,0)'); + self.yruler.attr('transform', 'translate(0,-500)'); + });*/ + this.mousex = this.mousey = -500; + + let sidebar = d3.select(this.element).append("div").attr("class", "sidebar"); + let title = sidebar.append("label").attr("class", "graph-title"); + + title.append("input") + .attr("type", "checkbox") + .attr("checked", "true") + .on("click", function () { self.toggle(); }); + title.append("span").text(this.name); + + this.legend = sidebar.append("div").attr("class", "legend"); + + this.resize = this.resize.bind(this); + this.render = this.render.bind(this); + this.averages = this.averages.bind(this); + + setInterval(this.averages, 1000); + + this.resize(); +} + +Graph.prototype = { + + /** + * These margin are used to properly position the SVG graph items inside the + * container element. + */ + margin: { + top: 10, + right: 150, + bottom: 20, + left: 50 + }, + + /** + * A Graph can be collapsed by the user. + */ + toggle: function () { + if (this.enabled) { + this.element.classList.add("disabled"); + this.enabled = false; + } else { + this.element.classList.remove("disabled"); + this.enabled = true; + } + Monitor.resize(); + }, + + /** + * If the container element is resized (e.g. because the window was resized or + * a scrollbar dis/appeared), the graph needs to be resized as well. + */ + resize: function () { + let style = getComputedStyle(this.element), + height = parseFloat(style.height) - this.margin.top - this.margin.bottom, + width = parseFloat(style.width) - this.margin.left - this.margin.right; + + d3.select(this.element).select("svg") + .attr("width", width + this.margin.left) + .attr("height", height + this.margin.top + this.margin.bottom); + + this.x.range([0, width]); + this.y.range([height, 0]); + + this.xelement.attr("transform", "translate(0," + height + ")"); + this.xruler.select("line[stroke-dasharray]").attr("y2", -height); + this.yruler.select("line[stroke-dasharray]").attr("x2", width); + }, + + /** + * If the domain of the Graph's data changes (on the time axis and/or on the + * value axis), the axes' domains need to be updated and the graph items need + * to be rescaled in order to represent all the data. + */ + rescale: function () { + let gettime = v => { return v.time; }, + getvalue = v => { return v.value; }, + ignored = c => { return this.ignored.has(c.id); }; + + let xmin = null, xmax = null, ymin = null, ymax = null; + for (let curve of this.curves.values()) { + if (ignored(curve)) { + continue; + } + if (xmax == null || curve.xmax > xmax) { + xmax = curve.xmax; + } + if (xmin == null || curve.xmin < xmin) { + xmin = curve.xmin; + } + if (ymax == null || curve.ymax > ymax) { + ymax = curve.ymax; + } + if (ymin == null || curve.ymin < ymin) { + ymin = curve.ymin; + } + } + for (let event of this.events.values()) { + if (ignored(event)) { + continue; + } + if (xmax == null || event.xmax > xmax) { + xmax = event.xmax; + } + if (xmin == null || event.xmin < xmin) { + xmin = event.xmin; + } + } + + let oldxdomain = this.x.domain(); + if (xmin != null && xmax != null) { + this.x.domain([xmin, xmax]); + let newxdomain = this.x.domain(); + if (newxdomain[0] !== oldxdomain[0] || newxdomain[1] !== oldxdomain[1]) { + this.xelement.call(this.xaxis); + } + } + + let oldydomain = this.y.domain(); + if (ymin != null && ymax != null) { + this.y.domain([ymin, ymax]).nice(); + let newydomain = this.y.domain(); + if (newydomain[0] !== oldydomain[0] || newydomain[1] !== oldydomain[1]) { + this.yelement.call(this.yaxis); + } + } + }, + + /** + * Add new values to the graph. + */ + update: function (data) { + delete data.graph; + + let time = data.time || Date.now(); + delete data.time; + + let curve = data.curve; + delete data.curve; + + // Single curve value, e.g. { curve: 'memory', value: 42, time: 1234 }. + if ("value" in data) { + this.push(this.curves, curve, [{time: time, value: data.value}]); + delete data.value; + } + + // Several curve values, e.g. { curve: 'memory', values: [{value: 42, time: 1234}] }. + if ("values" in data) { + this.push(this.curves, curve, data.values); + delete data.values; + } + + // Punctual event, e.g. { event: 'gc', time: 1234 }, + // event with duration, e.g. { event: 'jank', duration: 425, time: 1234 }. + if ("event" in data) { + this.push(this.events, data.event, [{time: time, value: data.duration}]); + delete data.event; + delete data.duration; + } + + // Remaining keys are curves, e.g. { time: 1234, memory: 42, battery: 13, temperature: 45 }. + for (let key in data) { + this.push(this.curves, key, [{time: time, value: data[key]}]); + } + + // If no render is currently pending, request one. + if (this.enabled && !this.request) { + this.request = requestAnimationFrame(this.render); + } + }, + + /** + * Insert new data into the graph's data structures. + */ + push: function (collection, id, values) { + + // Note: collection is either `this.curves` or `this.events`. + let item = collection.get(id); + if (!item) { + item = { id: id, values: [], xmin: null, xmax: null, ymin: 0, ymax: null, average: 0 }; + collection.set(id, item); + } + + for (let v of values) { + let time = new Date(v.time), value = +v.value; + // Update the curve/event's domain values. + if (item.xmax == null || time > item.xmax) { + item.xmax = time; + } + if (item.xmin == null || time < item.xmin) { + item.xmin = time; + } + if (item.ymax == null || value > item.ymax) { + item.ymax = value; + } + if (item.ymin == null || value < item.ymin) { + item.ymin = value; + } + // Note: A curve's average is not computed here. Call `graph.averages()`. + item.values.push({ time: time, value: value }); + } + }, + + /** + * Render the SVG graph with curves, events, crosshair and legend. + */ + render: function () { + this.request = null; + this.rescale(); + + + // DATA + + let self = this, + getid = d => { return d.id; }, + gettime = d => { return d.time.getTime(); }, + getline = d => { return self.line(d.values); }, + getcolor = d => { return self.color(d.id); }, + getvalues = d => { return d.values; }, + ignored = d => { return self.ignored.has(d.id); }; + + // Convert our maps to arrays for d3. + let curvedata = [...this.curves.values()], + eventdata = [...this.events.values()], + data = curvedata.concat(eventdata); + + + // CURVES + + // Map curve data to curve elements. + let curves = this.svg.selectAll(".curve").data(curvedata, getid); + + // Create new curves (no element corresponding to the data). + curves.enter().append("g").attr("class", "curve").append("path") + .style("stroke", getcolor); + + // Delete old curves (elements corresponding to data not present anymore). + curves.exit().remove(); + + // Update all curves from data. + this.svg.selectAll(".curve").select("path") + .attr("d", d => { return ignored(d) ? "" : getline(d); }); + + let height = parseFloat(getComputedStyle(this.element).height) - this.margin.top - this.margin.bottom; + + + // EVENTS + + // Map event data to event elements. + let events = this.svg.selectAll(".event-slot").data(eventdata, getid); + + // Create new events. + events.enter().append("g").attr("class", "event-slot"); + + // Remove old events. + events.exit().remove(); + + // Get all occurences of an event, and map its data to them. + let lines = this.svg.selectAll(".event-slot") + .style("stroke", d => { return ignored(d) ? "none" : getcolor(d); }) + .selectAll(".event") + .data(getvalues, gettime); + + // Create new event occurrence. + lines.enter().append("line").attr("class", "event").attr("y2", height); + + // Delete old event occurrence. + lines.exit().remove(); + + // Update all event occurrences from data. + this.svg.selectAll(".event") + .attr("transform", d => { return "translate(" + self.x(d.time) + ",0)"; }); + + + // CROSSHAIR + + // TODO select curves and events, intersect with curves and show values/hovers + // e.g. look like http://code.shutterstock.com/rickshaw/examples/lines.html + + // Update crosshair labels on each axis. + this.xruler.select("text").text(self.xformat(self.x.invert(self.mousex))); + this.yruler.select("text").text(self.yformat(self.y.invert(self.mousey))); + + + // LEGEND + + // Map data to legend elements. + let legends = this.legend.selectAll("label").data(data, getid); + + // Update averages. + legends.attr("title", c => { return "Average: " + self.yformat(c.average); }); + + // Create new legends. + let newlegend = legends.enter().append("label"); + newlegend.append("input").attr("type", "checkbox").attr("checked", "true").on("click", function (c) { + if (ignored(c)) { + this.parentElement.classList.remove("disabled"); + self.ignored.delete(c.id); + } else { + this.parentElement.classList.add("disabled"); + self.ignored.add(c.id); + } + self.update({}); // if no re-render is pending, request one. + }); + newlegend.append("span").attr("class", "legend-color").style("background-color", getcolor); + newlegend.append("span").attr("class", "legend-id").text(getid); + + // Delete old legends. + legends.exit().remove(); + }, + + /** + * Returns a SI value formatter with a given precision. + */ + formatter: function (decimals) { + return value => { + // Don't use sub-unit SI prefixes (milli, micro, etc.). + if (Math.abs(value) < 1) return value.toFixed(decimals); + // SI prefix, e.g. 1234567 will give '1.2M' at precision 1. + let prefix = d3.formatPrefix(value); + return prefix.scale(value).toFixed(decimals) + prefix.symbol; + }; + }, + + /** + * Compute the average of each time series. + */ + averages: function () { + for (let c of this.curves.values()) { + let length = c.values.length; + if (length > 0) { + let total = 0; + c.values.forEach(v => total += v.value); + c.average = (total / length); + } + } + }, + + /** + * Bisect a time serie to find the data point immediately left of `time`. + */ + bisectTime: d3.bisector(d => d.time).left, + + /** + * Get all curve values at a given time. + */ + valuesAt: function (time) { + let values = { time: time }; + + for (let id of this.curves.keys()) { + let curve = this.curves.get(id); + + // Find the closest value just before `time`. + let i = this.bisectTime(curve.values, time); + if (i < 0) { + // Curve starts after `time`, use first value. + values[id] = curve.values[0].value; + } else if (i > curve.values.length - 2) { + // Curve ends before `time`, use last value. + values[id] = curve.values[curve.values.length - 1].value; + } else { + // Curve has two values around `time`, interpolate. + let v1 = curve.values[i], + v2 = curve.values[i + 1], + delta = (time - v1.time) / (v2.time - v1.time); + values[id] = v1.value + (v2.value - v1.time) * delta; + } + } + return values; + } + +}; diff --git a/devtools/client/webide/content/monitor.xhtml b/devtools/client/webide/content/monitor.xhtml new file mode 100644 index 000000000..552f3826c --- /dev/null +++ b/devtools/client/webide/content/monitor.xhtml @@ -0,0 +1,31 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/monitor.css" type="text/css"/> + <script src="chrome://devtools/content/shared/vendor/d3.js"></script> + <script type="application/javascript;version=1.8" src="monitor.js"></script> + </head> + <body> + + <div id="controls"> + <a href="https://developer.mozilla.org/docs/Tools/WebIDE/Monitor" target="_blank">&monitor_help;</a> + <a id="close">&deck_close;</a> + </div> + + <h1>&monitor_title;</h1> + + </body> +</html> diff --git a/devtools/client/webide/content/moz.build b/devtools/client/webide/content/moz.build new file mode 100644 index 000000000..aac3a838c --- /dev/null +++ b/devtools/client/webide/content/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ['jar.mn'] diff --git a/devtools/client/webide/content/newapp.js b/devtools/client/webide/content/newapp.js new file mode 100644 index 000000000..d47bfabec --- /dev/null +++ b/devtools/client/webide/content/newapp.js @@ -0,0 +1,175 @@ +/* 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"; + +var Cc = Components.classes; +var Cu = Components.utils; +var Ci = Components.interfaces; + +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm"); +const Services = require("Services"); +const {FileUtils} = require("resource://gre/modules/FileUtils.jsm"); +const {AppProjects} = require("devtools/client/webide/modules/app-projects"); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {getJSON} = require("devtools/client/shared/getjson"); + +XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm"); + +const TEMPLATES_URL = "devtools.webide.templatesURL"; + +var gTemplateList = null; + +// See bug 989619 +console.log = console.log.bind(console); +console.warn = console.warn.bind(console); +console.error = console.error.bind(console); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + let projectNameNode = document.querySelector("#project-name"); + projectNameNode.addEventListener("input", canValidate, true); + getTemplatesJSON(); +}, true); + +function getTemplatesJSON() { + getJSON(TEMPLATES_URL).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"); + templatelistNode.innerHTML = ""; + for (let template of list) { + let richlistitemNode = document.createElement("richlistitem"); + let imageNode = document.createElement("image"); + imageNode.setAttribute("src", template.icon); + let labelNode = document.createElement("label"); + labelNode.setAttribute("value", template.name); + let descriptionNode = document.createElement("description"); + descriptionNode.textContent = template.description; + let vboxNode = document.createElement("vbox"); + vboxNode.setAttribute("flex", "1"); + richlistitemNode.appendChild(imageNode); + vboxNode.appendChild(labelNode); + vboxNode.appendChild(descriptionNode); + richlistitemNode.appendChild(vboxNode); + templatelistNode.appendChild(richlistitemNode); + } + templatelistNode.selectedIndex = 0; + + /* Chrome mochitest support */ + let testOptions = window.arguments[0].testOptions; + if (testOptions) { + templatelistNode.selectedIndex = testOptions.index; + document.querySelector("#project-name").value = testOptions.name; + doOK(); + } + }, (e) => { + failAndBail("Can't download app templates: " + e); + }); +} + +function failAndBail(msg) { + let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService); + promptService.alert(window, "error", msg); + window.close(); +} + +function canValidate() { + let projectNameNode = document.querySelector("#project-name"); + let dialogNode = document.querySelector("dialog"); + if (projectNameNode.value.length > 0) { + dialogNode.removeAttribute("buttondisabledaccept"); + } else { + dialogNode.setAttribute("buttondisabledaccept", "true"); + } +} + +function doOK() { + let projectName = document.querySelector("#project-name").value; + + if (!projectName) { + console.error("No project name"); + return false; + } + + if (!gTemplateList) { + console.error("No template index"); + return false; + } + + let templatelistNode = document.querySelector("#templatelist"); + if (templatelistNode.selectedIndex < 0) { + console.error("No template selected"); + return false; + } + + let folder; + + /* Chrome mochitest support */ + let testOptions = window.arguments[0].testOptions; + if (testOptions) { + folder = testOptions.folder; + } else { + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, "Select directory where to create app directory", Ci.nsIFilePicker.modeGetFolder); + let res = fp.show(); + if (res == Ci.nsIFilePicker.returnCancel) { + console.error("No directory selected"); + return false; + } + folder = fp.file; + } + + // Create subfolder with fs-friendly name of project + let subfolder = projectName.replace(/[\\/:*?"<>|]/g, "").toLowerCase(); + let win = Services.wm.getMostRecentWindow("devtools:webide"); + folder.append(subfolder); + + try { + folder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } catch (e) { + win.UI.reportError("error_folderCreationFailed"); + window.close(); + return false; + } + + // Download boilerplate zip + let template = gTemplateList[templatelistNode.selectedIndex]; + let source = template.file; + let target = folder.clone(); + target.append(subfolder + ".zip"); + + let bail = (e) => { + console.error(e); + window.close(); + }; + + Downloads.fetch(source, target).then(() => { + ZipUtils.extractFiles(target, folder); + target.remove(false); + AppProjects.addPackaged(folder).then((project) => { + window.arguments[0].location = project.location; + AppManager.validateAndUpdateProject(project).then(() => { + if (project.manifest) { + project.manifest.name = projectName; + AppManager.writeManifest(project).then(() => { + AppManager.validateAndUpdateProject(project).then( + () => {window.close();}, bail); + }, bail); + } else { + bail("Manifest not found"); + } + }, bail); + }, bail); + }, bail); + + return false; +} diff --git a/devtools/client/webide/content/newapp.xul b/devtools/client/webide/content/newapp.xul new file mode 100644 index 000000000..7ff083519 --- /dev/null +++ b/devtools/client/webide/content/newapp.xul @@ -0,0 +1,33 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE window [ + <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="chrome://webide/skin/newapp.css"?> + +<dialog id="webide:newapp" title="&newAppWindowTitle;" + width="600" height="400" + buttons="accept,cancel" + ondialogaccept="return doOK();" + buttondisabledaccept="true" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script type="application/javascript" src="newapp.js"></script> + <label class="header-name" value="&newAppHeader;"/> + + <richlistbox id="templatelist" flex="1"> + <description>&newAppLoadingTemplate;</description> + </richlistbox> + <vbox> + <label class="header-name" control="project-name" value="&newAppProjectName;"/> + <textbox id="project-name"/> + </vbox> + +</dialog> diff --git a/devtools/client/webide/content/permissionstable.js b/devtools/client/webide/content/permissionstable.js new file mode 100644 index 000000000..22c74bd0d --- /dev/null +++ b/devtools/client/webide/content/permissionstable.js @@ -0,0 +1,78 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {Connection} = require("devtools/shared/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 == "runtime-global-actors") { + BuildUI(); + } +} + +function generateFields(json) { + let table = document.querySelector("table"); + 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); + } +} + +var 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() + .then(json => generateFields(json)); + } else { + CloseUI(); + } +} diff --git a/devtools/client/webide/content/permissionstable.xhtml b/devtools/client/webide/content/permissionstable.xhtml new file mode 100644 index 000000000..361cfece8 --- /dev/null +++ b/devtools/client/webide/content/permissionstable.xhtml @@ -0,0 +1,36 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/permissionstable.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> diff --git a/devtools/client/webide/content/prefs.js b/devtools/client/webide/content/prefs.js new file mode 100644 index 000000000..75f6233ba --- /dev/null +++ b/devtools/client/webide/content/prefs.js @@ -0,0 +1,108 @@ +/* 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 Cu = Components.utils; +const {Services} = Cu.import("resource://gre/modules/Services.jsm", {}); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + + // Listen to preference changes + let inputs = document.querySelectorAll("[data-pref]"); + for (let i of inputs) { + let pref = i.dataset.pref; + Services.prefs.addObserver(pref, FillForm, false); + i.addEventListener("change", SaveForm, false); + } + + // Buttons + document.querySelector("#close").onclick = CloseUI; + document.querySelector("#restore").onclick = RestoreDefaults; + document.querySelector("#manageComponents").onclick = ShowAddons; + + // Initialize the controls + FillForm(); + +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + let inputs = document.querySelectorAll("[data-pref]"); + for (let i of inputs) { + let pref = i.dataset.pref; + i.removeEventListener("change", SaveForm, false); + Services.prefs.removeObserver(pref, FillForm, false); + } +}, true); + +function CloseUI() { + window.parent.UI.openProject(); +} + +function ShowAddons() { + window.parent.Cmds.showAddons(); +} + +function FillForm() { + let inputs = document.querySelectorAll("[data-pref]"); + for (let i of inputs) { + let pref = i.dataset.pref; + let val = GetPref(pref); + if (i.type == "checkbox") { + i.checked = val; + } else { + i.value = val; + } + } +} + +function SaveForm(e) { + let inputs = document.querySelectorAll("[data-pref]"); + for (let i of inputs) { + let pref = i.dataset.pref; + if (i.type == "checkbox") { + SetPref(pref, i.checked); + } else { + SetPref(pref, i.value); + } + } +} + +function GetPref(name) { + let type = Services.prefs.getPrefType(name); + switch (type) { + case Services.prefs.PREF_STRING: + return Services.prefs.getCharPref(name); + case Services.prefs.PREF_INT: + return Services.prefs.getIntPref(name); + case Services.prefs.PREF_BOOL: + return Services.prefs.getBoolPref(name); + default: + throw new Error("Unknown type"); + } +} + +function SetPref(name, value) { + let type = Services.prefs.getPrefType(name); + switch (type) { + case Services.prefs.PREF_STRING: + return Services.prefs.setCharPref(name, value); + case Services.prefs.PREF_INT: + return Services.prefs.setIntPref(name, value); + case Services.prefs.PREF_BOOL: + return Services.prefs.setBoolPref(name, value); + default: + throw new Error("Unknown type"); + } +} + +function RestoreDefaults() { + let inputs = document.querySelectorAll("[data-pref]"); + for (let i of inputs) { + let pref = i.dataset.pref; + Services.prefs.clearUserPref(pref); + } +} diff --git a/devtools/client/webide/content/prefs.xhtml b/devtools/client/webide/content/prefs.xhtml new file mode 100644 index 000000000..726ca772c --- /dev/null +++ b/devtools/client/webide/content/prefs.xhtml @@ -0,0 +1,112 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/prefs.js"></script> + </head> + <body> + + <div id="controls"> + <a id="restore">&prefs_restore;</a> + <a id="manageComponents">&prefs_manage_components;</a> + <a id="close">&deck_close;</a> + </div> + + <h1>&prefs_title;</h1> + + <h2>&prefs_general_title;</h2> + + <ul> + <li> + <label title="&prefs_options_showeditor_tooltip;"> + <input type="checkbox" data-pref="devtools.webide.showProjectEditor"/> + <span>&prefs_options_showeditor;</span> + </label> + </li> + <li> + <label title="&prefs_options_rememberlastproject_tooltip;"> + <input type="checkbox" data-pref="devtools.webide.restoreLastProject"/> + <span>&prefs_options_rememberlastproject;</span> + </label> + </li> + <li> + <label title="&prefs_options_autoconnectruntime_tooltip;"> + <input type="checkbox" data-pref="devtools.webide.autoConnectRuntime"/> + <span>&prefs_options_autoconnectruntime;</span> + </label> + </li> + <li> + <label class="text-input" title="&prefs_options_templatesurl_tooltip;"> + <span>&prefs_options_templatesurl;</span> + <input data-pref="devtools.webide.templatesURL"/> + </label> + </li> + </ul> + + <h2>&prefs_editor_title;</h2> + + <ul> + <li> + <label><span>&prefs_options_tabsize;</span> + <select data-pref="devtools.editor.tabsize"> + <option value="2">2</option> + <option value="4">4</option> + <option value="8">8</option> + </select> + </label> + </li> + <li> + <label title="&prefs_options_expandtab_tooltip;"> + <input type="checkbox" data-pref="devtools.editor.expandtab"/> + <span>&prefs_options_expandtab;</span> + </label> + </li> + <li> + <label title="&prefs_options_detectindentation_tooltip;"> + <input type="checkbox" data-pref="devtools.editor.detectindentation"/> + <span>&prefs_options_detectindentation;</span> + </label> + </li> + <li> + <label title="&prefs_options_autocomplete_tooltip;"> + <input type="checkbox" data-pref="devtools.editor.autocomplete"/> + <span>&prefs_options_autocomplete;</span> + </label> + </li> + <li> + <label title="&prefs_options_autoclosebrackets_tooltip;"> + <input type="checkbox" data-pref="devtools.editor.autoclosebrackets"/> + <span>&prefs_options_autoclosebrackets;</span> + </label> + </li> + <li> + <label title="&prefs_options_autosavefiles_tooltip;"> + <input type="checkbox" data-pref="devtools.webide.autosaveFiles"/> + <span>&prefs_options_autosavefiles;</span> + </label> + </li> + <li> + <label><span>&prefs_options_keybindings;</span> + <select data-pref="devtools.editor.keymap"> + <option value="default">&prefs_options_keybindings_default;</option> + <option value="vim">Vim</option> + <option value="emacs">Emacs</option> + <option value="sublime">Sublime</option> + </select> + </label> + </li> + </ul> + + </body> +</html> diff --git a/devtools/client/webide/content/project-listing.js b/devtools/client/webide/content/project-listing.js new file mode 100644 index 000000000..5641f6c0c --- /dev/null +++ b/devtools/client/webide/content/project-listing.js @@ -0,0 +1,42 @@ +/* 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/. */ + +/* eslint-env browser */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const ProjectList = require("devtools/client/webide/modules/project-list"); + +var projectList = new ProjectList(window, window.parent); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, true); + document.getElementById("new-app").onclick = CreateNewApp; + document.getElementById("hosted-app").onclick = ImportHostedApp; + document.getElementById("packaged-app").onclick = ImportPackagedApp; + document.getElementById("refresh-tabs").onclick = RefreshTabs; + projectList.update(); + projectList.updateCommands(); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + projectList.destroy(); +}); + +function RefreshTabs() { + projectList.refreshTabs(); +} + +function CreateNewApp() { + projectList.newApp(); +} + +function ImportHostedApp() { + projectList.importHostedApp(); +} + +function ImportPackagedApp() { + projectList.importPackagedApp(); +} diff --git a/devtools/client/webide/content/project-listing.xhtml b/devtools/client/webide/content/project-listing.xhtml new file mode 100644 index 000000000..337befe5d --- /dev/null +++ b/devtools/client/webide/content/project-listing.xhtml @@ -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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/panel-listing.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/project-listing.js"></script> + </head> + <body> + <div id="project-panel"> + <div id="project-panel-box"> + <button class="panel-item project-panel-item-newapp" id="new-app">&projectMenu_newApp_label;</button> + <button class="panel-item project-panel-item-openpackaged" id="packaged-app">&projectMenu_importPackagedApp_label;</button> + <button class="panel-item project-panel-item-openhosted" id="hosted-app">&projectMenu_importHostedApp_label;</button> + <label class="panel-header">&projectPanel_myProjects;</label> + <div id="project-panel-projects"></div> + <label class="panel-header" id="panel-header-runtimeapps" hidden="true">&projectPanel_runtimeApps;</label> + <div id="project-panel-runtimeapps"/> + <label class="panel-header" id="panel-header-tabs" hidden="true">&projectPanel_tabs; + <button class="project-panel-item-refreshtabs refresh-icon" id="refresh-tabs" title="&projectMenu_refreshTabs_label;"></button> + </label> + <div id="project-panel-tabs"/> + </div> + </div> + </body> +</html> diff --git a/devtools/client/webide/content/project-panel.js b/devtools/client/webide/content/project-panel.js new file mode 100644 index 000000000..54eab8251 --- /dev/null +++ b/devtools/client/webide/content/project-panel.js @@ -0,0 +1,11 @@ +/* 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/. */ + +var ProjectPanel = { + // TODO: Expand function to save toggle state. + toggleSidebar: function () { + document.querySelector("#project-listing-panel").setAttribute("sidebar-displayed", true); + document.querySelector("#project-listing-splitter").setAttribute("sidebar-displayed", true); + } +}; diff --git a/devtools/client/webide/content/runtime-listing.js b/devtools/client/webide/content/runtime-listing.js new file mode 100644 index 000000000..0a1a40a2a --- /dev/null +++ b/devtools/client/webide/content/runtime-listing.js @@ -0,0 +1,66 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const RuntimeList = require("devtools/client/webide/modules/runtime-list"); + +var runtimeList = new RuntimeList(window, window.parent); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad, true); + document.getElementById("runtime-screenshot").onclick = TakeScreenshot; + document.getElementById("runtime-permissions").onclick = ShowPermissionsTable; + document.getElementById("runtime-details").onclick = ShowRuntimeDetails; + document.getElementById("runtime-disconnect").onclick = DisconnectRuntime; + document.getElementById("runtime-preferences").onclick = ShowDevicePreferences; + document.getElementById("runtime-settings").onclick = ShowSettings; + document.getElementById("runtime-panel-installsimulator").onclick = ShowAddons; + document.getElementById("runtime-panel-noadbhelper").onclick = ShowAddons; + document.getElementById("runtime-panel-nousbdevice").onclick = ShowTroubleShooting; + document.getElementById("refresh-devices").onclick = RefreshScanners; + runtimeList.update(); + runtimeList.updateCommands(); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + runtimeList.destroy(); +}); + +function TakeScreenshot() { + runtimeList.takeScreenshot(); +} + +function ShowRuntimeDetails() { + runtimeList.showRuntimeDetails(); +} + +function ShowPermissionsTable() { + runtimeList.showPermissionsTable(); +} + +function ShowDevicePreferences() { + runtimeList.showDevicePreferences(); +} + +function ShowSettings() { + runtimeList.showSettings(); +} + +function RefreshScanners() { + runtimeList.refreshScanners(); +} + +function DisconnectRuntime() { + window.parent.Cmds.disconnectRuntime(); +} + +function ShowAddons() { + runtimeList.showAddons(); +} + +function ShowTroubleShooting() { + runtimeList.showTroubleShooting(); +} diff --git a/devtools/client/webide/content/runtime-listing.xhtml b/devtools/client/webide/content/runtime-listing.xhtml new file mode 100644 index 000000000..f648fac12 --- /dev/null +++ b/devtools/client/webide/content/runtime-listing.xhtml @@ -0,0 +1,45 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/panel-listing.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/runtime-listing.js"></script> + </head> + <body> + <div id="runtime-panel"> + <div id="runtime-panel-box"> + <label class="panel-header">&runtimePanel_usb; + <button class="runtime-panel-item-refreshdevices refresh-icon" id="refresh-devices" title="&runtimePanel_refreshDevices_label;"></button> + </label> + <button class="panel-item" id="runtime-panel-nousbdevice">&runtimePanel_nousbdevice;</button> + <button class="panel-item" id="runtime-panel-noadbhelper">&runtimePanel_noadbhelper;</button> + <div id="runtime-panel-usb"></div> + <label class="panel-header" id="runtime-header-wifi">&runtimePanel_wifi;</label> + <div id="runtime-panel-wifi"></div> + <label class="panel-header">&runtimePanel_simulator;</label> + <div id="runtime-panel-simulator"></div> + <button class="panel-item" id="runtime-panel-installsimulator">&runtimePanel_installsimulator;</button> + <label class="panel-header">&runtimePanel_other;</label> + <div id="runtime-panel-other"></div> + <div id="runtime-actions"> + <button class="panel-item" id="runtime-details">&runtimeMenu_showDetails_label;</button> + <button class="panel-item" id="runtime-permissions">&runtimeMenu_showPermissionTable_label;</button> + <button class="panel-item" id="runtime-preferences">&runtimeMenu_showDevicePrefs_label;</button> + <button class="panel-item" id="runtime-settings">&runtimeMenu_showSettings_label;</button> + <button class="panel-item" id="runtime-screenshot">&runtimeMenu_takeScreenshot_label;</button> + <button class="panel-item" id="runtime-disconnect">&runtimeMenu_disconnect_label;</button> + </div> + </div> + </div> + </body> +</html> diff --git a/devtools/client/webide/content/runtime-panel.js b/devtools/client/webide/content/runtime-panel.js new file mode 100644 index 000000000..3646fa15c --- /dev/null +++ b/devtools/client/webide/content/runtime-panel.js @@ -0,0 +1,11 @@ +/* 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/. */ + +var RuntimePanel = { + // TODO: Expand function to save toggle state. + toggleSidebar: function () { + document.querySelector("#runtime-listing-panel").setAttribute("sidebar-displayed", true); + document.querySelector("#runtime-listing-splitter").setAttribute("sidebar-displayed", true); + } +}; diff --git a/devtools/client/webide/content/runtimedetails.js b/devtools/client/webide/content/runtimedetails.js new file mode 100644 index 000000000..dea423e81 --- /dev/null +++ b/devtools/client/webide/content/runtimedetails.js @@ -0,0 +1,153 @@ +/* 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/. */ + +var Cu = Components.utils; +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const {Connection} = require("devtools/shared/client/connection-manager"); +const {RuntimeTypes} = require("devtools/client/webide/modules/runtimes"); +const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties"); + +const UNRESTRICTED_HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Running_and_debugging_apps#Unrestricted_app_debugging_%28including_certified_apps_main_process_etc.%29"; + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.querySelector("#close").onclick = CloseUI; + document.querySelector("#devtools-check button").onclick = EnableCertApps; + document.querySelector("#adb-check button").onclick = RootADB; + document.querySelector("#unrestricted-privileges").onclick = function () { + window.parent.UI.openInBrowser(UNRESTRICTED_HELP_URL); + }; + AppManager.on("app-manager-update", OnAppManagerUpdate); + BuildUI(); + CheckLockState(); +}, 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 == "runtime-global-actors") { + BuildUI(); + CheckLockState(); + } +} + +function generateFields(json) { + let table = document.querySelector("table"); + 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); + } +} + +var 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() + .then(json => generateFields(json)); + } else { + CloseUI(); + } +} + +function CheckLockState() { + let adbCheckResult = document.querySelector("#adb-check > .yesno"); + let devtoolsCheckResult = document.querySelector("#devtools-check > .yesno"); + let flipCertPerfButton = document.querySelector("#devtools-check button"); + let adbRootButton = document.querySelector("#adb-check button"); + let flipCertPerfAction = document.querySelector("#devtools-check > .action"); + let adbRootAction = document.querySelector("#adb-check > .action"); + + let sYes = Strings.GetStringFromName("runtimedetails_checkyes"); + let sNo = Strings.GetStringFromName("runtimedetails_checkno"); + let sUnknown = Strings.GetStringFromName("runtimedetails_checkunknown"); + let sNotUSB = Strings.GetStringFromName("runtimedetails_notUSBDevice"); + + flipCertPerfButton.setAttribute("disabled", "true"); + flipCertPerfAction.setAttribute("hidden", "true"); + adbRootAction.setAttribute("hidden", "true"); + + adbCheckResult.textContent = sUnknown; + devtoolsCheckResult.textContent = sUnknown; + + if (AppManager.connection && + AppManager.connection.status == Connection.Status.CONNECTED) { + + // ADB check + if (AppManager.selectedRuntime.type === RuntimeTypes.USB) { + let device = AppManager.selectedRuntime.device; + if (device && device.summonRoot) { + device.isRoot().then(isRoot => { + if (isRoot) { + adbCheckResult.textContent = sYes; + flipCertPerfButton.removeAttribute("disabled"); + } else { + adbCheckResult.textContent = sNo; + adbRootAction.removeAttribute("hidden"); + } + }, e => console.error(e)); + } else { + adbCheckResult.textContent = sUnknown; + } + } else { + adbCheckResult.textContent = sNotUSB; + } + + // forbid-certified-apps check + try { + let prefFront = AppManager.preferenceFront; + prefFront.getBoolPref("devtools.debugger.forbid-certified-apps").then(isForbidden => { + if (isForbidden) { + devtoolsCheckResult.textContent = sNo; + flipCertPerfAction.removeAttribute("hidden"); + } else { + devtoolsCheckResult.textContent = sYes; + } + }, e => console.error(e)); + } catch (e) { + // Exception. pref actor is only accessible if forbird-certified-apps is false + devtoolsCheckResult.textContent = sNo; + flipCertPerfAction.removeAttribute("hidden"); + } + + } + +} + +function EnableCertApps() { + let device = AppManager.selectedRuntime.device; + // TODO: Remove `network.disable.ipc.security` once bug 1125916 is fixed. + device.shell( + "stop b2g && " + + "cd /data/b2g/mozilla/*.default/ && " + + "echo 'user_pref(\"devtools.debugger.forbid-certified-apps\", false);' >> prefs.js && " + + "echo 'user_pref(\"dom.apps.developer_mode\", true);' >> prefs.js && " + + "echo 'user_pref(\"network.disable.ipc.security\", true);' >> prefs.js && " + + "echo 'user_pref(\"dom.webcomponents.enabled\", true);' >> prefs.js && " + + "start b2g" + ); +} + +function RootADB() { + let device = AppManager.selectedRuntime.device; + device.summonRoot().then(CheckLockState, (e) => console.error(e)); +} diff --git a/devtools/client/webide/content/runtimedetails.xhtml b/devtools/client/webide/content/runtimedetails.xhtml new file mode 100644 index 000000000..b2f74728a --- /dev/null +++ b/devtools/client/webide/content/runtimedetails.xhtml @@ -0,0 +1,46 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/runtimedetails.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> + + <div id="devicePrivileges"> + <p id="adb-check"> + &runtimedetails_adbIsRoot;<span class="yesno"></span> + <div class="action"> + <button>&runtimedetails_summonADBRoot;</button> + <em>&runtimedetails_ADBRootWarning;</em> + </div> + </p> + <p id="devtools-check"> + <a id="unrestricted-privileges">&runtimedetails_unrestrictedPrivileges;</a><span class="yesno"></span> + <div class="action"> + <button>&runtimedetails_requestPrivileges;</button> + <em>&runtimedetails_privilegesWarning;</em> + </div> + </p> + </div> + + <table></table> + </body> +</html> diff --git a/devtools/client/webide/content/simulator.js b/devtools/client/webide/content/simulator.js new file mode 100644 index 000000000..ddc1cbed1 --- /dev/null +++ b/devtools/client/webide/content/simulator.js @@ -0,0 +1,352 @@ +/* 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/. */ + +var Cu = Components.utils; +var Ci = Components.interfaces; + +const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const { getDevices, getDeviceString } = require("devtools/client/shared/devices"); +const { Simulators, Simulator } = require("devtools/client/webide/modules/simulators"); +const Services = require("Services"); +const EventEmitter = require("devtools/shared/event-emitter"); +const promise = require("promise"); +const utils = require("devtools/client/webide/modules/utils"); + +const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties"); + +var SimulatorEditor = { + + // Available Firefox OS Simulator addons (key: `addon.id`). + _addons: {}, + + // Available device simulation profiles (key: `device.name`). + _devices: {}, + + // The names of supported simulation options. + _deviceOptions: [], + + // The <form> element used to edit Simulator options. + _form: null, + + // The Simulator object being edited. + _simulator: null, + + // Generate the dynamic form elements. + init() { + let promises = []; + + // Grab the <form> element. + let form = this._form; + if (!form) { + // This is the first time we run `init()`, bootstrap some things. + form = this._form = document.querySelector("#simulator-editor"); + form.addEventListener("change", this.update.bind(this)); + Simulators.on("configure", (e, simulator) => { this.edit(simulator); }); + // Extract the list of device simulation options we'll support. + let deviceFields = form.querySelectorAll("*[data-device]"); + this._deviceOptions = Array.map(deviceFields, field => field.name); + } + + // Append a new <option> to a <select> (or <optgroup>) element. + function opt(select, value, text) { + let option = document.createElement("option"); + option.value = value; + option.textContent = text; + select.appendChild(option); + } + + // Generate B2G version selector. + promises.push(Simulators.findSimulatorAddons().then(addons => { + this._addons = {}; + form.version.innerHTML = ""; + form.version.classList.remove("custom"); + addons.forEach(addon => { + this._addons[addon.id] = addon; + opt(form.version, addon.id, addon.name); + }); + opt(form.version, "custom", ""); + opt(form.version, "pick", Strings.GetStringFromName("simulator_custom_binary")); + })); + + // Generate profile selector. + form.profile.innerHTML = ""; + form.profile.classList.remove("custom"); + opt(form.profile, "default", Strings.GetStringFromName("simulator_default_profile")); + opt(form.profile, "custom", ""); + opt(form.profile, "pick", Strings.GetStringFromName("simulator_custom_profile")); + + // Generate example devices list. + form.device.innerHTML = ""; + form.device.classList.remove("custom"); + opt(form.device, "custom", Strings.GetStringFromName("simulator_custom_device")); + promises.push(getDevices().then(devices => { + devices.TYPES.forEach(type => { + let b2gDevices = devices[type].filter(d => d.firefoxOS); + if (b2gDevices.length < 1) { + return; + } + let optgroup = document.createElement("optgroup"); + optgroup.label = getDeviceString(type); + b2gDevices.forEach(device => { + this._devices[device.name] = device; + opt(optgroup, device.name, device.name); + }); + form.device.appendChild(optgroup); + }); + })); + + return promise.all(promises); + }, + + // Edit the configuration of an existing Simulator, or create a new one. + edit(simulator) { + // If no Simulator was given to edit, we're creating a new one. + if (!simulator) { + simulator = new Simulator(); // Default options. + Simulators.add(simulator); + } + + this._simulator = null; + + return this.init().then(() => { + this._simulator = simulator; + + // Update the form fields. + this._form.name.value = simulator.name; + + this.updateVersionSelector(); + this.updateProfileSelector(); + this.updateDeviceSelector(); + this.updateDeviceFields(); + + // Change visibility of 'TV Simulator Menu'. + let tvSimMenu = document.querySelector("#tv_simulator_menu"); + tvSimMenu.style.visibility = (this._simulator.type === "television") ? + "visible" : "hidden"; + + // Trigger any listener waiting for this update + let change = document.createEvent("HTMLEvents"); + change.initEvent("change", true, true); + this._form.dispatchEvent(change); + }); + }, + + // Open the directory of TV Simulator config. + showTVConfigDirectory() { + let profD = Services.dirsvc.get("ProfD", Ci.nsIFile); + profD.append("extensions"); + profD.append(this._simulator.addon.id); + profD.append("profile"); + profD.append("dummy"); + let profileDir = profD.path; + + // Show the profile directory. + let nsLocalFile = Components.Constructor("@mozilla.org/file/local;1", + "nsILocalFile", "initWithPath"); + new nsLocalFile(profileDir).reveal(); + }, + + // Close the configuration panel. + close() { + this._simulator = null; + window.parent.UI.openProject(); + }, + + // Restore the simulator to its default configuration. + restoreDefaults() { + let simulator = this._simulator; + this.version = simulator.addon.id; + this.profile = "default"; + simulator.restoreDefaults(); + Simulators.emitUpdated(); + return this.edit(simulator); + }, + + // Delete this simulator. + deleteSimulator() { + Simulators.remove(this._simulator); + this.close(); + }, + + // Select an available option, or set the "custom" option. + updateSelector(selector, value) { + selector.value = value; + if (selector.selectedIndex == -1) { + selector.value = "custom"; + selector.classList.add("custom"); + selector[selector.selectedIndex].textContent = value; + } + }, + + // VERSION: Can be an installed `addon.id` or a custom binary path. + + get version() { + return this._simulator.options.b2gBinary || this._simulator.addon.id; + }, + + set version(value) { + let form = this._form; + let simulator = this._simulator; + let oldVer = simulator.version; + if (this._addons[value]) { + // `value` is a simulator addon ID. + simulator.addon = this._addons[value]; + simulator.options.b2gBinary = null; + } else { + // `value` is a custom binary path. + simulator.options.b2gBinary = value; + // TODO (Bug 1146531) Indicate that a custom profile is now required. + } + // If `form.name` contains the old version, update its last occurrence. + if (form.name.value.includes(oldVer) && simulator.version !== oldVer) { + let regex = new RegExp("(.*)" + oldVer); + let name = form.name.value.replace(regex, "$1" + simulator.version); + simulator.options.name = form.name.value = Simulators.uniqueName(name); + } + }, + + updateVersionSelector() { + this.updateSelector(this._form.version, this.version); + }, + + // PROFILE. Can be "default" or a custom profile directory path. + + get profile() { + return this._simulator.options.gaiaProfile || "default"; + }, + + set profile(value) { + this._simulator.options.gaiaProfile = (value == "default" ? null : value); + }, + + updateProfileSelector() { + this.updateSelector(this._form.profile, this.profile); + }, + + // DEVICE. Can be an existing `device.name` or "custom". + + get device() { + let devices = this._devices; + let simulator = this._simulator; + + // Search for the name of a device matching current simulator options. + for (let name in devices) { + let match = true; + for (let option of this._deviceOptions) { + if (simulator.options[option] === devices[name][option]) { + continue; + } + match = false; + break; + } + if (match) { + return name; + } + } + return "custom"; + }, + + set device(name) { + let device = this._devices[name]; + if (!device) { + return; + } + let form = this._form; + let simulator = this._simulator; + this._deviceOptions.forEach(option => { + simulator.options[option] = form[option].value = device[option] || null; + }); + // TODO (Bug 1146531) Indicate when a custom profile is required (e.g. for + // tablet, TV…). + }, + + updateDeviceSelector() { + this.updateSelector(this._form.device, this.device); + }, + + // Erase any current values, trust only the `simulator.options`. + updateDeviceFields() { + let form = this._form; + let simulator = this._simulator; + this._deviceOptions.forEach(option => { + form[option].value = simulator.options[option]; + }); + }, + + // Handle a change in our form's fields. + update(event) { + let simulator = this._simulator; + if (!simulator) { + return; + } + let form = this._form; + let input = event.target; + switch (input.name) { + case "name": + simulator.options.name = input.value; + break; + case "version": + switch (input.value) { + case "pick": + let file = utils.getCustomBinary(window); + if (file) { + this.version = file.path; + } + // Whatever happens, don't stay on the "pick" option. + this.updateVersionSelector(); + break; + case "custom": + this.version = input[input.selectedIndex].textContent; + break; + default: + this.version = input.value; + } + break; + case "profile": + switch (input.value) { + case "pick": + let directory = utils.getCustomProfile(window); + if (directory) { + this.profile = directory.path; + } + // Whatever happens, don't stay on the "pick" option. + this.updateProfileSelector(); + break; + case "custom": + this.profile = input[input.selectedIndex].textContent; + break; + default: + this.profile = input.value; + } + break; + case "device": + this.device = input.value; + break; + default: + simulator.options[input.name] = input.value || null; + this.updateDeviceSelector(); + } + Simulators.emitUpdated(); + }, +}; + +window.addEventListener("load", function onLoad() { + document.querySelector("#close").onclick = e => { + SimulatorEditor.close(); + }; + document.querySelector("#reset").onclick = e => { + SimulatorEditor.restoreDefaults(); + }; + document.querySelector("#remove").onclick = e => { + SimulatorEditor.deleteSimulator(); + }; + + // We just loaded, so we probably missed the first configure request. + SimulatorEditor.edit(Simulators._lastConfiguredSimulator); + + document.querySelector("#open-tv-dummy-directory").onclick = e => { + SimulatorEditor.showTVConfigDirectory(); + e.preventDefault(); + }; +}); diff --git a/devtools/client/webide/content/simulator.xhtml b/devtools/client/webide/content/simulator.xhtml new file mode 100644 index 000000000..3ab916248 --- /dev/null +++ b/devtools/client/webide/content/simulator.xhtml @@ -0,0 +1,99 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/simulator.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/simulator.js"></script> + </head> + <body> + + <div id="controls"> + <a id="remove" class="hidden">&simulator_remove;</a> + <a id="reset">&simulator_reset;</a> + <a id="close">&deck_close;</a> + </div> + + <form id="simulator-editor"> + + <h1>&simulator_title;</h1> + + <h2>&simulator_software;</h2> + + <ul> + <li> + <label> + <span class="label">&simulator_name;</span> + <input type="text" name="name"/> + </label> + </li> + <li> + <label> + <span class="label">&simulator_version;</span> + <select name="version"/> + </label> + </li> + <li> + <label> + <span class="label">&simulator_profile;</span> + <select name="profile"/> + </label> + </li> + </ul> + + <h2>&simulator_hardware;</h2> + + <ul> + <li> + <label> + <span class="label">&simulator_device;</span> + <select name="device"/> + </label> + </li> + <li> + <label> + <span class="label">&simulator_screenSize;</span> + <input name="width" data-device="" type="number"/> + <span>×</span> + <input name="height" data-device="" type="number"/> + </label> + </li> + <li class="hidden"> + <label> + <span class="label">&simulator_pixelRatio;</span> + <input name="pixelRatio" data-device="" type="number" step="0.05"/> + </label> + </li> + </ul> + + <!-- This menu is shown when simulator type is television--> + <p id="tv_simulator_menu" style="visibility:hidden;"> + <h2>&simulator_tv_data;</h2> + + <ul> + <li> + <label> + <span class="label">&simulator_tv_data_open;</span> + <button id="open-tv-dummy-directory"> + &simulator_tv_data_open_button; + </button> + </label> + </li> + </ul> + + </p> + + </form> + + </body> +</html> diff --git a/devtools/client/webide/content/webide.js b/devtools/client/webide/content/webide.js new file mode 100644 index 000000000..c222332e3 --- /dev/null +++ b/devtools/client/webide/content/webide.js @@ -0,0 +1,1157 @@ +/* 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/. */ + +var Cc = Components.classes; +var Cu = Components.utils; +var Ci = Components.interfaces; + +const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {}); +const {gDevTools} = require("devtools/client/framework/devtools"); +const {gDevToolsBrowser} = require("devtools/client/framework/devtools-browser"); +const {Toolbox} = require("devtools/client/framework/toolbox"); +const Services = require("Services"); +const {AppProjects} = require("devtools/client/webide/modules/app-projects"); +const {Connection} = require("devtools/shared/client/connection-manager"); +const {AppManager} = require("devtools/client/webide/modules/app-manager"); +const EventEmitter = require("devtools/shared/event-emitter"); +const promise = require("promise"); +const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor"); +const {GetAvailableAddons} = require("devtools/client/webide/modules/addons"); +const {getJSON} = require("devtools/client/shared/getjson"); +const utils = require("devtools/client/webide/modules/utils"); +const Telemetry = require("devtools/client/shared/telemetry"); +const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes"); +const {showDoorhanger} = require("devtools/client/shared/doorhanger"); +const {Simulators} = require("devtools/client/webide/modules/simulators"); +const {Task} = require("devtools/shared/task"); + +const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties"); + +const HTML = "http://www.w3.org/1999/xhtml"; +const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting"; + +const MAX_ZOOM = 1.4; +const MIN_ZOOM = 0.6; + +const MS_PER_DAY = 86400000; + +[["AppManager", AppManager], + ["AppProjects", AppProjects], + ["Connection", Connection]].forEach(([key, value]) => { + Object.defineProperty(this, key, { + value: value, + enumerable: true, + writable: false + }); + }); + +// Download remote resources early +getJSON("devtools.webide.addonsURL"); +getJSON("devtools.webide.templatesURL"); +getJSON("devtools.devices.url"); + +// See bug 989619 +console.log = console.log.bind(console); +console.warn = console.warn.bind(console); +console.error = console.error.bind(console); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + UI.init(); +}); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + UI.destroy(); +}); + +var UI = { + init: function () { + this._telemetry = new Telemetry(); + this._telemetry.toolOpened("webide"); + + AppManager.init(); + + this.appManagerUpdate = this.appManagerUpdate.bind(this); + AppManager.on("app-manager-update", this.appManagerUpdate); + + Cmds.showProjectPanel(); + Cmds.showRuntimePanel(); + + this.updateCommands(); + + this.onfocus = this.onfocus.bind(this); + window.addEventListener("focus", this.onfocus, true); + + AppProjects.load().then(() => { + this.autoSelectProject(); + }, e => { + console.error(e); + this.reportError("error_appProjectsLoadFailed"); + }); + + // Auto install the ADB Addon Helper and Tools Adapters. Only once. + // If the user decides to uninstall any of this addon, we won't install it again. + let autoinstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper"); + let autoinstallFxdtAdapters = Services.prefs.getBoolPref("devtools.webide.autoinstallFxdtAdapters"); + if (autoinstallADBHelper) { + GetAvailableAddons().then(addons => { + addons.adb.install(); + }, console.error); + } + if (autoinstallFxdtAdapters) { + GetAvailableAddons().then(addons => { + addons.adapters.install(); + }, console.error); + } + Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false); + Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", false); + + if (Services.prefs.getBoolPref("devtools.webide.widget.autoinstall") && + !Services.prefs.getBoolPref("devtools.webide.widget.enabled")) { + Services.prefs.setBoolPref("devtools.webide.widget.enabled", true); + gDevToolsBrowser.moveWebIDEWidgetInNavbar(); + } + + this.setupDeck(); + + this.contentViewer = window.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .contentViewer; + this.contentViewer.fullZoom = Services.prefs.getCharPref("devtools.webide.zoom"); + + gDevToolsBrowser.isWebIDEInitialized.resolve(); + + this.configureSimulator = this.configureSimulator.bind(this); + Simulators.on("configure", this.configureSimulator); + }, + + destroy: function () { + window.removeEventListener("focus", this.onfocus, true); + AppManager.off("app-manager-update", this.appManagerUpdate); + AppManager.destroy(); + Simulators.off("configure", this.configureSimulator); + this.updateConnectionTelemetry(); + this._telemetry.toolClosed("webide"); + this._telemetry.toolClosed("webideProjectEditor"); + this._telemetry.destroy(); + }, + + canCloseProject: function () { + if (this.projecteditor) { + return this.projecteditor.confirmUnsaved(); + } + return true; + }, + + onfocus: function () { + // Because we can't track the activity in the folder project, + // we need to validate the project regularly. Let's assume that + // if a modification happened, it happened when the window was + // not focused. + if (AppManager.selectedProject && + AppManager.selectedProject.type != "mainProcess" && + AppManager.selectedProject.type != "runtimeApp" && + AppManager.selectedProject.type != "tab") { + AppManager.validateAndUpdateProject(AppManager.selectedProject); + } + + // Hook to display promotional Developer Edition doorhanger. Only displayed once. + // Hooked into the `onfocus` event because sometimes does not work + // when run at the end of `init`. ¯\(°_o)/¯ + showDoorhanger({ window, type: "deveditionpromo", anchor: document.querySelector("#deck") }); + }, + + appManagerUpdate: function (event, what, details) { + // Got a message from app-manager.js + // See AppManager.update() for descriptions of what these events mean. + switch (what) { + case "runtime-list": + this.autoConnectRuntime(); + break; + case "connection": + this.updateRuntimeButton(); + this.updateCommands(); + this.updateConnectionTelemetry(); + break; + case "before-project": + if (!this.canCloseProject()) { + details.cancel(); + } + break; + case "project": + this._updatePromise = Task.spawn(function* () { + UI.updateTitle(); + yield UI.destroyToolbox(); + UI.updateCommands(); + UI.openProject(); + yield UI.autoStartProject(); + UI.autoOpenToolbox(); + UI.saveLastSelectedProject(); + UI.updateRemoveProjectButton(); + }); + return; + case "project-started": + this.updateCommands(); + UI.autoOpenToolbox(); + break; + case "project-stopped": + UI.destroyToolbox(); + this.updateCommands(); + break; + case "runtime-global-actors": + // Check runtime version only on runtime-global-actors, + // as we expect to use device actor + this.checkRuntimeVersion(); + this.updateCommands(); + break; + case "runtime-details": + this.updateRuntimeButton(); + break; + case "runtime": + this.updateRuntimeButton(); + this.saveLastConnectedRuntime(); + break; + case "project-validated": + this.updateTitle(); + this.updateCommands(); + this.updateProjectEditorHeader(); + break; + case "install-progress": + this.updateProgress(Math.round(100 * details.bytesSent / details.totalBytes)); + break; + case "runtime-targets": + this.autoSelectProject(); + break; + case "pre-package": + this.prePackageLog(details); + break; + } + this._updatePromise = promise.resolve(); + }, + + configureSimulator: function (event, simulator) { + UI.selectDeckPanel("simulator"); + }, + + openInBrowser: function (url) { + // Open a URL in a Firefox window + let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType); + if (mainWindow) { + mainWindow.openUILinkIn(url, "tab"); + mainWindow.focus() + } else { + window.open(url); + } + }, + + updateTitle: function () { + let project = AppManager.selectedProject; + if (project) { + window.document.title = Strings.formatStringFromName("title_app", [project.name], 1); + } else { + window.document.title = Strings.GetStringFromName("title_noApp"); + } + }, + + /** ******** BUSY UI **********/ + + _busyTimeout: null, + _busyOperationDescription: null, + _busyPromise: null, + + updateProgress: function (percent) { + let progress = document.querySelector("#action-busy-determined"); + progress.mode = "determined"; + progress.value = percent; + this.setupBusyTimeout(); + }, + + busy: function () { + let win = document.querySelector("window"); + win.classList.add("busy"); + win.classList.add("busy-undetermined"); + this.updateCommands(); + this.update("busy"); + }, + + unbusy: function () { + let win = document.querySelector("window"); + win.classList.remove("busy"); + win.classList.remove("busy-determined"); + win.classList.remove("busy-undetermined"); + this.updateCommands(); + this.update("unbusy"); + this._busyPromise = null; + }, + + setupBusyTimeout: function () { + this.cancelBusyTimeout(); + this._busyTimeout = setTimeout(() => { + this.unbusy(); + UI.reportError("error_operationTimeout", this._busyOperationDescription); + }, Services.prefs.getIntPref("devtools.webide.busyTimeout")); + }, + + cancelBusyTimeout: function () { + clearTimeout(this._busyTimeout); + }, + + busyWithProgressUntil: function (promise, operationDescription) { + let busy = this.busyUntil(promise, operationDescription); + let win = document.querySelector("window"); + let progress = document.querySelector("#action-busy-determined"); + progress.mode = "undetermined"; + win.classList.add("busy-determined"); + win.classList.remove("busy-undetermined"); + return busy; + }, + + busyUntil: function (promise, operationDescription) { + // Freeze the UI until the promise is resolved. A timeout will unfreeze the + // UI, just in case the promise never gets resolved. + this._busyPromise = promise; + this._busyOperationDescription = operationDescription; + this.setupBusyTimeout(); + this.busy(); + promise.then(() => { + this.cancelBusyTimeout(); + this.unbusy(); + }, (e) => { + let message; + if (e && e.error && e.message) { + // Some errors come from fronts that are not based on protocol.js. + // Errors are not translated to strings. + message = operationDescription + " (" + e.error + "): " + e.message; + } else { + message = operationDescription + (e ? (": " + e) : ""); + } + this.cancelBusyTimeout(); + let operationCanceled = e && e.canceled; + if (!operationCanceled) { + UI.reportError("error_operationFail", message); + if (e) { + console.error(e); + } + } + this.unbusy(); + }); + return promise; + }, + + reportError: function (l10nProperty, ...l10nArgs) { + let text; + + if (l10nArgs.length > 0) { + text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length); + } else { + text = Strings.GetStringFromName(l10nProperty); + } + + console.error(text); + + let buttons = [{ + label: Strings.GetStringFromName("notification_showTroubleShooting_label"), + accessKey: Strings.GetStringFromName("notification_showTroubleShooting_accesskey"), + callback: function () { + Cmds.showTroubleShooting(); + } + }]; + + let nbox = document.querySelector("#notificationbox"); + nbox.removeAllNotifications(true); + nbox.appendNotification(text, "webide:errornotification", null, + nbox.PRIORITY_WARNING_LOW, buttons); + }, + + dismissErrorNotification: function () { + let nbox = document.querySelector("#notificationbox"); + nbox.removeAllNotifications(true); + }, + + /** ******** COMMANDS **********/ + + /** + * This module emits various events when state changes occur. + * + * The events this module may emit include: + * busy: + * The window is currently busy and certain UI functions may be disabled. + * unbusy: + * The window is not busy and certain UI functions may be re-enabled. + */ + update: function (what, details) { + this.emit("webide-update", what, details); + }, + + updateCommands: function () { + // Action commands + let playCmd = document.querySelector("#cmd_play"); + let stopCmd = document.querySelector("#cmd_stop"); + let debugCmd = document.querySelector("#cmd_toggleToolbox"); + let playButton = document.querySelector("#action-button-play"); + let projectPanelCmd = document.querySelector("#cmd_showProjectPanel"); + + if (document.querySelector("window").classList.contains("busy")) { + playCmd.setAttribute("disabled", "true"); + stopCmd.setAttribute("disabled", "true"); + debugCmd.setAttribute("disabled", "true"); + projectPanelCmd.setAttribute("disabled", "true"); + return; + } + + if (!AppManager.selectedProject || !AppManager.connected) { + playCmd.setAttribute("disabled", "true"); + stopCmd.setAttribute("disabled", "true"); + debugCmd.setAttribute("disabled", "true"); + } else { + let isProjectRunning = AppManager.isProjectRunning(); + if (isProjectRunning) { + playButton.classList.add("reload"); + stopCmd.removeAttribute("disabled"); + debugCmd.removeAttribute("disabled"); + } else { + playButton.classList.remove("reload"); + stopCmd.setAttribute("disabled", "true"); + debugCmd.setAttribute("disabled", "true"); + } + + // If connected and a project is selected + if (AppManager.selectedProject.type == "runtimeApp") { + playCmd.removeAttribute("disabled"); + } else if (AppManager.selectedProject.type == "tab") { + playCmd.removeAttribute("disabled"); + stopCmd.setAttribute("disabled", "true"); + } else if (AppManager.selectedProject.type == "mainProcess") { + playCmd.setAttribute("disabled", "true"); + stopCmd.setAttribute("disabled", "true"); + } else { + if (AppManager.selectedProject.errorsCount == 0 && + AppManager.runtimeCanHandleApps()) { + playCmd.removeAttribute("disabled"); + } else { + playCmd.setAttribute("disabled", "true"); + } + } + } + + // Runtime commands + let monitorCmd = document.querySelector("#cmd_showMonitor"); + let screenshotCmd = document.querySelector("#cmd_takeScreenshot"); + let permissionsCmd = document.querySelector("#cmd_showPermissionsTable"); + let detailsCmd = document.querySelector("#cmd_showRuntimeDetails"); + let disconnectCmd = document.querySelector("#cmd_disconnectRuntime"); + let devicePrefsCmd = document.querySelector("#cmd_showDevicePrefs"); + let settingsCmd = document.querySelector("#cmd_showSettings"); + + if (AppManager.connected) { + if (AppManager.deviceFront) { + monitorCmd.removeAttribute("disabled"); + detailsCmd.removeAttribute("disabled"); + permissionsCmd.removeAttribute("disabled"); + screenshotCmd.removeAttribute("disabled"); + } + if (AppManager.preferenceFront) { + devicePrefsCmd.removeAttribute("disabled"); + } + if (AppManager.settingsFront) { + settingsCmd.removeAttribute("disabled"); + } + disconnectCmd.removeAttribute("disabled"); + } else { + monitorCmd.setAttribute("disabled", "true"); + detailsCmd.setAttribute("disabled", "true"); + permissionsCmd.setAttribute("disabled", "true"); + screenshotCmd.setAttribute("disabled", "true"); + disconnectCmd.setAttribute("disabled", "true"); + devicePrefsCmd.setAttribute("disabled", "true"); + settingsCmd.setAttribute("disabled", "true"); + } + + let runtimePanelButton = document.querySelector("#runtime-panel-button"); + + if (AppManager.connected) { + runtimePanelButton.setAttribute("active", "true"); + runtimePanelButton.removeAttribute("hidden"); + } else { + runtimePanelButton.removeAttribute("active"); + runtimePanelButton.setAttribute("hidden", "true"); + } + + projectPanelCmd.removeAttribute("disabled"); + }, + + updateRemoveProjectButton: function () { + // Remove command + let removeCmdNode = document.querySelector("#cmd_removeProject"); + if (AppManager.selectedProject) { + removeCmdNode.removeAttribute("disabled"); + } else { + removeCmdNode.setAttribute("disabled", "true"); + } + }, + + /** ******** RUNTIME **********/ + + get lastConnectedRuntime() { + return Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime"); + }, + + set lastConnectedRuntime(runtime) { + Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime", runtime); + }, + + autoConnectRuntime: function () { + // Automatically reconnect to the previously selected runtime, + // if available and has an ID and feature is enabled + if (AppManager.selectedRuntime || + !Services.prefs.getBoolPref("devtools.webide.autoConnectRuntime") || + !this.lastConnectedRuntime) { + return; + } + let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/); + + type = type.toLowerCase(); + + // Local connection is mapped to AppManager.runtimeList.other array + if (type == "local") { + type = "other"; + } + + // We support most runtimes except simulator, that needs to be manually + // launched + if (type == "usb" || type == "wifi" || type == "other") { + for (let runtime of AppManager.runtimeList[type]) { + // Some runtimes do not expose an id and don't support autoconnect (like + // remote connection) + if (runtime.id == id) { + // Only want one auto-connect attempt, so clear last runtime value + this.lastConnectedRuntime = ""; + this.connectToRuntime(runtime); + } + } + } + }, + + connectToRuntime: function (runtime) { + let name = runtime.name; + let promise = AppManager.connectToRuntime(runtime); + promise.then(() => this.initConnectionTelemetry()) + .catch(() => { + // Empty rejection handler to silence uncaught rejection warnings + // |busyUntil| will listen for rejections. + // Bug 1121100 may find a better way to silence these. + }); + promise = this.busyUntil(promise, "Connecting to " + name); + // Stop busy timeout for runtimes that take unknown or long amounts of time + // to connect. + if (runtime.prolongedConnection) { + this.cancelBusyTimeout(); + } + return promise; + }, + + updateRuntimeButton: function () { + let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label"); + if (!AppManager.selectedRuntime) { + labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label")); + } else { + let name = AppManager.selectedRuntime.name; + labelNode.setAttribute("value", name); + } + }, + + saveLastConnectedRuntime: function () { + if (AppManager.selectedRuntime && + AppManager.selectedRuntime.id !== undefined) { + this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + + AppManager.selectedRuntime.id; + } else { + this.lastConnectedRuntime = ""; + } + }, + + /** ******** ACTIONS **********/ + + _actionsToLog: new Set(), + + /** + * For each new connection, track whether play and debug were ever used. Only + * one value is collected for each button, even if they are used multiple + * times during a connection. + */ + initConnectionTelemetry: function () { + this._actionsToLog.add("play"); + this._actionsToLog.add("debug"); + }, + + /** + * Action occurred. Log that it happened, and remove it from the loggable + * set. + */ + onAction: function (action) { + if (!this._actionsToLog.has(action)) { + return; + } + this.logActionState(action, true); + this._actionsToLog.delete(action); + }, + + /** + * Connection status changed or we are shutting down. Record any loggable + * actions as having not occurred. + */ + updateConnectionTelemetry: function () { + for (let action of this._actionsToLog.values()) { + this.logActionState(action, false); + } + this._actionsToLog.clear(); + }, + + logActionState: function (action, state) { + let histogramId = "DEVTOOLS_WEBIDE_CONNECTION_" + + action.toUpperCase() + "_USED"; + this._telemetry.log(histogramId, state); + }, + + /** ******** PROJECTS **********/ + + // ProjectEditor & details screen + + destroyProjectEditor: function () { + if (this.projecteditor) { + this.projecteditor.destroy(); + this.projecteditor = null; + } + }, + + /** + * Called when selecting or deselecting the project editor panel. + */ + onChangeProjectEditorSelected: function () { + if (this.projecteditor) { + let panel = document.querySelector("#deck").selectedPanel; + if (panel && panel.id == "deck-panel-projecteditor") { + this.projecteditor.menuEnabled = true; + this._telemetry.toolOpened("webideProjectEditor"); + } else { + this.projecteditor.menuEnabled = false; + this._telemetry.toolClosed("webideProjectEditor"); + } + } + }, + + getProjectEditor: function () { + if (this.projecteditor) { + return this.projecteditor.loaded; + } + + let projecteditorIframe = document.querySelector("#deck-panel-projecteditor"); + this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe, { + menubar: document.querySelector("#main-menubar"), + menuindex: 1 + }); + this.projecteditor.on("onEditorSave", () => { + AppManager.validateAndUpdateProject(AppManager.selectedProject); + this._telemetry.actionOccurred("webideProjectEditorSave"); + }); + return this.projecteditor.loaded; + }, + + updateProjectEditorHeader: function () { + let project = AppManager.selectedProject; + if (!project || !this.projecteditor) { + return; + } + let status = project.validationStatus || "unknown"; + if (status == "error warning") { + status = "error"; + } + this.getProjectEditor().then((projecteditor) => { + projecteditor.setProjectToAppPath(project.location, { + name: project.name, + iconUrl: project.icon, + projectOverviewURL: "chrome://webide/content/details.xhtml", + validationStatus: status + }).then(null, console.error); + }, console.error); + }, + + isProjectEditorEnabled: function () { + return Services.prefs.getBoolPref("devtools.webide.showProjectEditor"); + }, + + openProject: function () { + let project = AppManager.selectedProject; + + // Nothing to show + + if (!project) { + this.resetDeck(); + return; + } + + // Make sure the directory exist before we show Project Editor + + let forceDetailsOnly = false; + if (project.type == "packaged") { + forceDetailsOnly = !utils.doesFileExist(project.location); + } + + // Show only the details screen + + if (project.type != "packaged" || + !this.isProjectEditorEnabled() || + forceDetailsOnly) { + this.selectDeckPanel("details"); + return; + } + + // Show ProjectEditor + + this.getProjectEditor().then(() => { + this.updateProjectEditorHeader(); + }, console.error); + + this.selectDeckPanel("projecteditor"); + }, + + autoStartProject: Task.async(function* () { + let project = AppManager.selectedProject; + + if (!project) { + return; + } + if (!(project.type == "runtimeApp" || + project.type == "mainProcess" || + project.type == "tab")) { + return; // For something that is not an editable app, we're done. + } + + // Do not force opening apps that are already running, as they may have + // some activity being opened and don't want to dismiss them. + if (project.type == "runtimeApp" && !AppManager.isProjectRunning()) { + yield UI.busyUntil(AppManager.launchRuntimeApp(), "running app"); + } + }), + + autoOpenToolbox: Task.async(function* () { + let project = AppManager.selectedProject; + + if (!project) { + return; + } + if (!(project.type == "runtimeApp" || + project.type == "mainProcess" || + project.type == "tab")) { + return; // For something that is not an editable app, we're done. + } + + yield UI.createToolbox(); + }), + + importAndSelectApp: Task.async(function* (source) { + let isPackaged = !!source.path; + let project; + try { + project = yield AppProjects[isPackaged ? "addPackaged" : "addHosted"](source); + } catch (e) { + if (e === "Already added") { + // Select project that's already been added, + // and allow it to be revalidated and selected + project = AppProjects.get(isPackaged ? source.path : source); + } else { + throw e; + } + } + + // Select project + AppManager.selectedProject = project; + + this._telemetry.actionOccurred("webideImportProject"); + }), + + // Remember the last selected project on the runtime + saveLastSelectedProject: function () { + let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject"); + if (!shouldRestore) { + return; + } + + // Ignore unselection of project on runtime disconnection + if (!AppManager.connected) { + return; + } + + let project = "", type = ""; + let selected = AppManager.selectedProject; + if (selected) { + if (selected.type == "runtimeApp") { + type = "runtimeApp"; + project = selected.app.manifestURL; + } else if (selected.type == "mainProcess") { + type = "mainProcess"; + } else if (selected.type == "packaged" || + selected.type == "hosted") { + type = "local"; + project = selected.location; + } + } + if (type) { + Services.prefs.setCharPref("devtools.webide.lastSelectedProject", + type + ":" + project); + } else { + Services.prefs.clearUserPref("devtools.webide.lastSelectedProject"); + } + }, + + autoSelectProject: function () { + if (AppManager.selectedProject) { + return; + } + let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject"); + if (!shouldRestore) { + return; + } + let pref = Services.prefs.getCharPref("devtools.webide.lastSelectedProject"); + if (!pref) { + return; + } + let m = pref.match(/^(\w+):(.*)$/); + if (!m) { + return; + } + let [_, type, project] = m; + + if (type == "local") { + let lastProject = AppProjects.get(project); + if (lastProject) { + AppManager.selectedProject = lastProject; + } + } + + // For other project types, we need to be connected to the runtime + if (!AppManager.connected) { + return; + } + + if (type == "mainProcess" && AppManager.isMainProcessDebuggable()) { + AppManager.selectedProject = { + type: "mainProcess", + name: Strings.GetStringFromName("mainProcess_label"), + icon: AppManager.DEFAULT_PROJECT_ICON + }; + } else if (type == "runtimeApp") { + let app = AppManager.apps.get(project); + if (app) { + AppManager.selectedProject = { + type: "runtimeApp", + app: app.manifest, + icon: app.iconURL, + name: app.manifest.name + }; + } + } + }, + + /** ******** DECK **********/ + + setupDeck: function () { + let iframes = document.querySelectorAll("#deck > iframe"); + for (let iframe of iframes) { + iframe.tooltip = "aHTMLTooltip"; + } + }, + + resetFocus: function () { + document.commandDispatcher.focusedElement = document.documentElement; + }, + + selectDeckPanel: function (id) { + let deck = document.querySelector("#deck"); + if (deck.selectedPanel && deck.selectedPanel.id === "deck-panel-" + id) { + // This panel is already displayed. + return; + } + this.resetFocus(); + let panel = deck.querySelector("#deck-panel-" + id); + let lazysrc = panel.getAttribute("lazysrc"); + if (lazysrc) { + panel.removeAttribute("lazysrc"); + panel.setAttribute("src", lazysrc); + } + deck.selectedPanel = panel; + this.onChangeProjectEditorSelected(); + }, + + resetDeck: function () { + this.resetFocus(); + let deck = document.querySelector("#deck"); + deck.selectedPanel = null; + this.onChangeProjectEditorSelected(); + }, + + buildIDToDate(buildID) { + let fields = buildID.match(/(\d{4})(\d{2})(\d{2})/); + // Date expects 0 - 11 for months + return new Date(fields[1], Number.parseInt(fields[2]) - 1, fields[3]); + }, + + checkRuntimeVersion: Task.async(function* () { + if (AppManager.connected && AppManager.deviceFront) { + let desc = yield AppManager.deviceFront.getDescription(); + // Compare device and firefox build IDs + // and only compare by day (strip hours/minutes) to prevent + // warning against builds of the same day. + let deviceID = desc.appbuildid.substr(0, 8); + let localID = Services.appinfo.appBuildID.substr(0, 8); + let deviceDate = this.buildIDToDate(deviceID); + let localDate = this.buildIDToDate(localID); + // Allow device to be newer by up to a week. This accommodates those with + // local device builds, since their devices will almost always be newer + // than the client. + if (deviceDate - localDate > 7 * MS_PER_DAY) { + this.reportError("error_runtimeVersionTooRecent", deviceID, localID); + } + } + }), + + /** ******** TOOLBOX **********/ + + /** + * There are many ways to close a toolbox: + * * Close button inside the toolbox + * * Toggle toolbox wrench in WebIDE + * * Disconnect the current runtime gracefully + * * Yank cord out of device + * * Close or crash the app/tab + * We can't know for sure which one was used here, so reset the + * |toolboxPromise| since someone must be destroying it to reach here, + * and call our own close method. + */ + _onToolboxClosed: function (promise, iframe) { + // Only save toolbox size, disable wrench button, workaround focus issue... + // if we are closing the last toolbox: + // - toolboxPromise is nullified by destroyToolbox and is still null here + // if no other toolbox has been opened in between, + // - having two distinct promise means we are receiving closed event + // for a previous, non-current, toolbox. + if (!this.toolboxPromise || this.toolboxPromise === promise) { + this.toolboxPromise = null; + this.resetFocus(); + Services.prefs.setIntPref("devtools.toolbox.footer.height", iframe.height); + + let splitter = document.querySelector(".devtools-horizontal-splitter"); + splitter.setAttribute("hidden", "true"); + document.querySelector("#action-button-debug").removeAttribute("active"); + } + // We have to destroy the iframe, otherwise, the keybindings of webide don't work + // properly anymore. + iframe.remove(); + }, + + destroyToolbox: function () { + // Only have a live toolbox if |this.toolboxPromise| exists + if (this.toolboxPromise) { + let toolboxPromise = this.toolboxPromise; + this.toolboxPromise = null; + return toolboxPromise.then(toolbox => toolbox.destroy()); + } + return promise.resolve(); + }, + + createToolbox: function () { + // If |this.toolboxPromise| exists, there is already a live toolbox + if (this.toolboxPromise) { + return this.toolboxPromise; + } + + let iframe = document.createElement("iframe"); + iframe.id = "toolbox"; + + // Compute a uid on the iframe in order to identify toolbox iframe + // when receiving toolbox-close event + iframe.uid = new Date().getTime(); + + let height = Services.prefs.getIntPref("devtools.toolbox.footer.height"); + iframe.height = height; + + let promise = this.toolboxPromise = AppManager.getTarget().then(target => { + return this._showToolbox(target, iframe); + }).then(toolbox => { + // Destroy the toolbox on WebIDE side before + // toolbox.destroy's promise resolves. + toolbox.once("destroyed", this._onToolboxClosed.bind(this, promise, iframe)); + return toolbox; + }, console.error); + + return this.busyUntil(this.toolboxPromise, "opening toolbox"); + }, + + _showToolbox: function (target, iframe) { + let splitter = document.querySelector(".devtools-horizontal-splitter"); + splitter.removeAttribute("hidden"); + + document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling); + let host = Toolbox.HostType.CUSTOM; + let options = { customIframe: iframe, zoom: false, uid: iframe.uid }; + + document.querySelector("#action-button-debug").setAttribute("active", "true"); + + return gDevTools.showToolbox(target, null, host, options); + }, + + prePackageLog: function (msg) { + if (msg == "start") { + UI.selectDeckPanel("logs"); + } + } +}; + +EventEmitter.decorate(UI); + +var Cmds = { + quit: function () { + if (UI.canCloseProject()) { + window.close(); + } + }, + + showProjectPanel: function () { + ProjectPanel.toggleSidebar(); + return promise.resolve(); + }, + + showRuntimePanel: function () { + RuntimeScanners.scan(); + RuntimePanel.toggleSidebar(); + }, + + disconnectRuntime: function () { + let disconnecting = Task.spawn(function* () { + yield UI.destroyToolbox(); + yield AppManager.disconnectRuntime(); + }); + return UI.busyUntil(disconnecting, "disconnecting from runtime"); + }, + + takeScreenshot: function () { + let url = AppManager.deviceFront.screenshotToDataURL(); + return UI.busyUntil(url.then(longstr => { + return longstr.string().then(dataURL => { + longstr.release().then(null, console.error); + UI.openInBrowser(dataURL); + }); + }), "taking screenshot"); + }, + + showPermissionsTable: function () { + UI.selectDeckPanel("permissionstable"); + }, + + showRuntimeDetails: function () { + UI.selectDeckPanel("runtimedetails"); + }, + + showDevicePrefs: function () { + UI.selectDeckPanel("devicepreferences"); + }, + + showSettings: function () { + UI.selectDeckPanel("devicesettings"); + }, + + showMonitor: function () { + UI.selectDeckPanel("monitor"); + }, + + play: Task.async(function* () { + let busy; + switch (AppManager.selectedProject.type) { + case "packaged": + let autosave = + Services.prefs.getBoolPref("devtools.webide.autosaveFiles"); + if (autosave && UI.projecteditor) { + yield UI.projecteditor.saveAllFiles(); + } + busy = UI.busyWithProgressUntil(AppManager.installAndRunProject(), + "installing and running app"); + break; + case "hosted": + busy = UI.busyUntil(AppManager.installAndRunProject(), + "installing and running app"); + break; + case "runtimeApp": + busy = UI.busyUntil(AppManager.launchOrReloadRuntimeApp(), "launching / reloading app"); + break; + case "tab": + busy = UI.busyUntil(AppManager.reloadTab(), "reloading tab"); + break; + } + if (!busy) { + return promise.reject(); + } + UI.onAction("play"); + return busy; + }), + + stop: function () { + return UI.busyUntil(AppManager.stopRunningApp(), "stopping app"); + }, + + toggleToolbox: function () { + UI.onAction("debug"); + if (UI.toolboxPromise) { + UI.destroyToolbox(); + return promise.resolve(); + } else { + return UI.createToolbox(); + } + }, + + removeProject: function () { + AppManager.removeSelectedProject(); + }, + + toggleEditors: function () { + let isNowEnabled = !UI.isProjectEditorEnabled(); + Services.prefs.setBoolPref("devtools.webide.showProjectEditor", isNowEnabled); + if (!isNowEnabled) { + UI.destroyProjectEditor(); + } + UI.openProject(); + }, + + showTroubleShooting: function () { + UI.openInBrowser(HELP_URL); + }, + + showAddons: function () { + UI.selectDeckPanel("addons"); + }, + + showPrefs: function () { + UI.selectDeckPanel("prefs"); + }, + + zoomIn: function () { + if (UI.contentViewer.fullZoom < MAX_ZOOM) { + UI.contentViewer.fullZoom += 0.1; + Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom); + } + }, + + zoomOut: function () { + if (UI.contentViewer.fullZoom > MIN_ZOOM) { + UI.contentViewer.fullZoom -= 0.1; + Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom); + } + }, + + resetZoom: function () { + UI.contentViewer.fullZoom = 1; + Services.prefs.setCharPref("devtools.webide.zoom", 1); + } +}; diff --git a/devtools/client/webide/content/webide.xul b/devtools/client/webide/content/webide.xul new file mode 100644 index 000000000..a3e4355b9 --- /dev/null +++ b/devtools/client/webide/content/webide.xul @@ -0,0 +1,178 @@ +<?xml version="1.0"?> + +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE window [ + <!ENTITY % webideDTD SYSTEM "chrome://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?> + +<?xml-stylesheet href="chrome://global/skin/global.css"?> +<?xml-stylesheet href="resource://devtools/client/themes/common.css"?> +<?xml-stylesheet href="chrome://webide/skin/webide.css"?> + +<window id="webide" onclose="return UI.canCloseProject();" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="&windowTitle;" + windowtype="devtools:webide" + macanimationtype="document" + fullscreenbutton="true" + screenX="4" screenY="4" + width="800" height="600" + persist="screenX screenY width height sizemode"> + + <script type="application/javascript" src="chrome://global/content/globalOverlay.js"></script> + <script type="application/javascript" src="project-panel.js"></script> + <script type="application/javascript" src="runtime-panel.js"></script> + <script type="application/javascript" src="webide.js"></script> + + <commandset id="mainCommandSet"> + <commandset id="editMenuCommands"/> + <commandset id="webideCommands"> + <command id="cmd_quit" oncommand="Cmds.quit()"/> + <command id="cmd_newApp" oncommand="Cmds.newApp()" label="&projectMenu_newApp_label;"/> + <command id="cmd_importPackagedApp" oncommand="Cmds.importPackagedApp()" label="&projectMenu_importPackagedApp_label;"/> + <command id="cmd_importHostedApp" oncommand="Cmds.importHostedApp()" label="&projectMenu_importHostedApp_label;"/> + <command id="cmd_showDevicePrefs" label="&runtimeMenu_showDevicePrefs_label;" oncommand="Cmds.showDevicePrefs()"/> + <command id="cmd_showSettings" label="&runtimeMenu_showSettings_label;" oncommand="Cmds.showSettings()"/> + <command id="cmd_removeProject" oncommand="Cmds.removeProject()" label="&projectMenu_remove_label;"/> + <command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/> + <command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/> + <command id="cmd_disconnectRuntime" oncommand="Cmds.disconnectRuntime()" label="&runtimeMenu_disconnect_label;"/> + <command id="cmd_showMonitor" oncommand="Cmds.showMonitor()" label="&runtimeMenu_showMonitor_label;"/> + <command id="cmd_showPermissionsTable" oncommand="Cmds.showPermissionsTable()" label="&runtimeMenu_showPermissionTable_label;"/> + <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_showPrefs" oncommand="Cmds.showPrefs()"/> + <command id="cmd_showTroubleShooting" oncommand="Cmds.showTroubleShooting()"/> + <command id="cmd_play" oncommand="Cmds.play()"/> + <command id="cmd_stop" oncommand="Cmds.stop()" label="&projectMenu_stop_label;"/> + <command id="cmd_toggleToolbox" oncommand="Cmds.toggleToolbox()"/> + <command id="cmd_zoomin" label="&viewMenu_zoomin_label;" oncommand="Cmds.zoomIn()"/> + <command id="cmd_zoomout" label="&viewMenu_zoomout_label;" oncommand="Cmds.zoomOut()"/> + <command id="cmd_resetzoom" label="&viewMenu_resetzoom_label;" oncommand="Cmds.resetZoom()"/> + </commandset> + </commandset> + + <menubar id="main-menubar"> + <menu id="menu-project" label="&projectMenu_label;" accesskey="&projectMenu_accesskey;"> + <menupopup id="menu-project-popup"> + <menuitem command="cmd_newApp" accesskey="&projectMenu_newApp_accesskey;"/> + <menuitem command="cmd_importPackagedApp" accesskey="&projectMenu_importPackagedApp_accesskey;"/> + <menuitem command="cmd_importHostedApp" accesskey="&projectMenu_importHostedApp_accesskey;"/> + <menuitem id="menuitem-show_projectPanel" command="cmd_showProjectPanel" key="key_showProjectPanel" label="&projectMenu_selectApp_label;" accesskey="&projectMenu_selectApp_accesskey;"/> + <menuseparator/> + <menuitem command="cmd_play" key="key_play" label="&projectMenu_play_label;" accesskey="&projectMenu_play_accesskey;"/> + <menuitem command="cmd_stop" accesskey="&projectMenu_stop_accesskey;"/> + <menuitem command="cmd_toggleToolbox" key="key_toggleToolbox" label="&projectMenu_debug_label;" accesskey="&projectMenu_debug_accesskey;"/> + <menuseparator/> + <menuitem command="cmd_removeProject" accesskey="&projectMenu_remove_accesskey;"/> + <menuseparator/> + <menuitem command="cmd_showPrefs" label="&projectMenu_showPrefs_label;" accesskey="&projectMenu_showPrefs_accesskey;"/> + <menuitem command="cmd_showAddons" label="&projectMenu_manageComponents_label;" accesskey="&projectMenu_manageComponents_accesskey;"/> + </menupopup> + </menu> + + <menu id="menu-runtime" label="&runtimeMenu_label;" accesskey="&runtimeMenu_accesskey;"> + <menupopup id="menu-runtime-popup"> + <menuitem command="cmd_showMonitor" accesskey="&runtimeMenu_showMonitor_accesskey;"/> + <menuitem command="cmd_takeScreenshot" accesskey="&runtimeMenu_takeScreenshot_accesskey;"/> + <menuitem command="cmd_showPermissionsTable" accesskey="&runtimeMenu_showPermissionTable_accesskey;"/> + <menuitem command="cmd_showRuntimeDetails" accesskey="&runtimeMenu_showDetails_accesskey;"/> + <menuitem command="cmd_showDevicePrefs" accesskey="&runtimeMenu_showDevicePrefs_accesskey;"/> + <menuitem command="cmd_showSettings" accesskey="&runtimeMenu_showSettings_accesskey;"/> + <menuseparator/> + <menuitem command="cmd_disconnectRuntime" accesskey="&runtimeMenu_disconnect_accesskey;"/> + </menupopup> + </menu> + + <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;"/> + <menuseparator/> + <menuitem command="cmd_zoomin" key="key_zoomin" accesskey="&viewMenu_zoomin_accesskey;"/> + <menuitem command="cmd_zoomout" key="key_zoomout" accesskey="&viewMenu_zoomout_accesskey;"/> + <menuitem command="cmd_resetzoom" key="key_resetzoom" accesskey="&viewMenu_resetzoom_accesskey;"/> + </menupopup> + </menu> + + </menubar> + + <keyset id="mainKeyset"> + <key key="&key_quit;" id="key_quit" command="cmd_quit" modifiers="accel"/> + <key key="&key_showProjectPanel;" id="key_showProjectPanel" command="cmd_showProjectPanel" modifiers="accel"/> + <key key="&key_play;" id="key_play" command="cmd_play" modifiers="accel"/> + <key key="&key_toggleEditor;" id="key_toggleEditor" command="cmd_toggleEditor" modifiers="accel"/> + <key keycode="&key_toggleToolbox;" id="key_toggleToolbox" command="cmd_toggleToolbox"/> + <key key="&key_zoomin;" id="key_zoomin" command="cmd_zoomin" modifiers="accel"/> + <key key="&key_zoomin2;" id="key_zoomin2" command="cmd_zoomin" modifiers="accel"/> + <key key="&key_zoomout;" id="key_zoomout" command="cmd_zoomout" modifiers="accel"/> + <key key="&key_resetzoom;" id="key_resetzoom" command="cmd_resetzoom" modifiers="accel"/> + </keyset> + + <tooltip id="aHTMLTooltip" page="true"/> + + <toolbar id="main-toolbar"> + + <vbox flex="1"> + <hbox id="action-buttons-container" class="busy"> + <toolbarbutton id="action-button-play" class="action-button" command="cmd_play" tooltiptext="&projectMenu_play_label;"/> + <toolbarbutton id="action-button-stop" class="action-button" command="cmd_stop" tooltiptext="&projectMenu_stop_label;"/> + <toolbarbutton id="action-button-debug" class="action-button" command="cmd_toggleToolbox" tooltiptext="&projectMenu_debug_label;"/> + <hbox id="action-busy" align="center"> + <html:img id="action-busy-undetermined" src="chrome://webide/skin/throbber.svg"/> + <progressmeter id="action-busy-determined"/> + </hbox> + </hbox> + + <hbox id="panel-buttons-container"> + <spacer flex="1"/> + <toolbarbutton id="runtime-panel-button" class="panel-button"> + <image class="panel-button-image"/> + <label class="panel-button-label" value="&runtimeButton_label;"/> + </toolbarbutton> + </hbox> + + </vbox> + </toolbar> + + <notificationbox flex="1" id="notificationbox"> + <div flex="1" id="deck-panels"> + <vbox id="project-listing-panel" class="project-listing panel-list" flex="1"> + <div id="project-listing-wrapper" class="panel-list-wrapper"> + <iframe id="project-listing-panel-details" flex="1" src="project-listing.xhtml" tooltip="aHTMLTooltip"/> + </div> + </vbox> + <splitter class="devtools-side-splitter" id="project-listing-splitter"/> + <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-prefs" flex="1" src="prefs.xhtml"/> + <iframe id="deck-panel-permissionstable" flex="1" lazysrc="permissionstable.xhtml"/> + <iframe id="deck-panel-runtimedetails" flex="1" lazysrc="runtimedetails.xhtml"/> + <iframe id="deck-panel-monitor" flex="1" lazysrc="monitor.xhtml"/> + <iframe id="deck-panel-devicepreferences" flex="1" lazysrc="devicepreferences.xhtml"/> + <iframe id="deck-panel-devicesettings" flex="1" lazysrc="devicesettings.xhtml"/> + <iframe id="deck-panel-logs" flex="1" src="logs.xhtml"/> + <iframe id="deck-panel-simulator" flex="1" lazysrc="simulator.xhtml"/> + </deck> + <splitter class="devtools-side-splitter" id="runtime-listing-splitter"/> + <vbox id="runtime-listing-panel" class="runtime-listing panel-list" flex="1"> + <div id="runtime-listing-wrapper" class="panel-list-wrapper"> + <iframe id="runtime-listing-panel-details" flex="1" src="runtime-listing.xhtml" tooltip="aHTMLTooltip"/> + </div> + </vbox> + </div> + <splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/> + <!-- toolbox iframe will be inserted here --> + </notificationbox> + +</window> diff --git a/devtools/client/webide/content/wifi-auth.js b/devtools/client/webide/content/wifi-auth.js new file mode 100644 index 000000000..5ae5d824c --- /dev/null +++ b/devtools/client/webide/content/wifi-auth.js @@ -0,0 +1,44 @@ +/* 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"; + +var Cu = Components.utils; +const { require } = + Cu.import("resource://devtools/shared/Loader.jsm", {}); +const Services = require("Services"); +const QR = require("devtools/shared/qrcode/index"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.getElementById("close").onclick = () => window.close(); + document.getElementById("no-scanner").onclick = showToken; + document.getElementById("yes-scanner").onclick = hideToken; + buildUI(); +}); + +function buildUI() { + let { oob } = window.arguments[0]; + createQR(oob); + createToken(oob); +} + +function createQR(oob) { + let oobData = JSON.stringify(oob); + let imgData = QR.encodeToDataURI(oobData, "L" /* low quality */); + document.querySelector("#qr-code img").src = imgData.src; +} + +function createToken(oob) { + let token = oob.sha256.replace(/:/g, "").toLowerCase() + oob.k; + document.querySelector("#token pre").textContent = token; +} + +function showToken() { + document.querySelector("body").setAttribute("token", "true"); +} + +function hideToken() { + document.querySelector("body").removeAttribute("token"); +} diff --git a/devtools/client/webide/content/wifi-auth.xhtml b/devtools/client/webide/content/wifi-auth.xhtml new file mode 100644 index 000000000..cfeec3c96 --- /dev/null +++ b/devtools/client/webide/content/wifi-auth.xhtml @@ -0,0 +1,45 @@ +<?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://devtools/locale/webide.dtd" > + %webideDTD; +]> + +<html id="devtools:wifi-auth" xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf8"/> + <link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/> + <link rel="stylesheet" href="chrome://webide/skin/wifi-auth.css" type="text/css"/> + <script type="application/javascript;version=1.8" src="chrome://webide/content/wifi-auth.js"></script> + </head> + <body> + + <div id="controls"> + <a id="close">&deck_close;</a> + </div> + + <h3 id="header">&wifi_auth_header;</h3> + <div id="scan-request">&wifi_auth_scan_request;</div> + + <div id="qr-code"> + <div id="qr-code-wrapper"> + <img/> + </div> + <a id="no-scanner" class="toggle-scanner">&wifi_auth_no_scanner;</a> + <div id="qr-size-note"> + <h5>&wifi_auth_qr_size_note;</h5> + </div> + </div> + + <div id="token"> + <div>&wifi_auth_token_request;</div> + <pre id="token-value"/> + <a id="yes-scanner" class="toggle-scanner">&wifi_auth_yes_scanner;</a> + </div> + + </body> +</html> |