From df89b48e3060726eb799debc185b7d80b50dcf37 Mon Sep 17 00:00:00 2001
From: Roy Tam
Date: Fri, 18 Oct 2019 09:09:39 +0800
Subject: [PATCH] Revert "Remove WebIDE devtools component."
This reverts commit 8c1500707fd7d6aadc6d3bd6262e2419aad3b0f5.
# Conflicts:
# application/basilisk/components/customizableui/CustomizableUI.jsm
---
.../customizableui/CustomizableUI.jsm | 5 +
devtools/client/framework/devtools-browser.js | 17 +
devtools/client/framework/gDevTools.jsm | 11 +
.../client/locales/en-US/menus.properties | 5 +
devtools/client/locales/en-US/webide.dtd | 222 ++++
.../client/locales/en-US/webide.properties | 92 ++
devtools/client/menus.js | 11 +
devtools/client/moz.build | 1 +
devtools/client/preferences/devtools.js | 3 +
.../plugins/app-manager/app-project-editor.js | 56 +
.../lib/plugins/app-manager/moz.build | 10 +
.../lib/plugins/app-manager/plugin.js | 77 ++
.../projecteditor/lib/plugins/moz.build | 1 +
.../client/projecteditor/lib/projecteditor.js | 1 +
devtools/client/shared/telemetry.js | 17 +
devtools/client/webide/components/moz.build | 10 +
.../client/webide/components/webideCli.js | 58 +
.../components/webideComponents.manifest | 4 +
devtools/client/webide/content/addons.js | 135 ++
devtools/client/webide/content/addons.xhtml | 31 +
devtools/client/webide/content/details.js | 139 ++
devtools/client/webide/content/details.xhtml | 54 +
.../webide/content/devicepreferences.js | 81 ++
.../webide/content/devicepreferences.xhtml | 49 +
.../client/webide/content/devicesettings.js | 81 ++
.../webide/content/devicesettings.xhtml | 50 +
devtools/client/webide/content/jar.mn | 38 +
devtools/client/webide/content/logs.js | 70 +
devtools/client/webide/content/logs.xhtml | 33 +
devtools/client/webide/content/monitor.js | 741 +++++++++++
devtools/client/webide/content/monitor.xhtml | 31 +
devtools/client/webide/content/moz.build | 7 +
devtools/client/webide/content/newapp.js | 175 +++
devtools/client/webide/content/newapp.xul | 33 +
.../client/webide/content/permissionstable.js | 78 ++
.../webide/content/permissionstable.xhtml | 36 +
devtools/client/webide/content/prefs.js | 108 ++
devtools/client/webide/content/prefs.xhtml | 112 ++
.../client/webide/content/project-listing.js | 42 +
.../webide/content/project-listing.xhtml | 35 +
.../client/webide/content/project-panel.js | 11 +
.../client/webide/content/runtime-listing.js | 66 +
.../webide/content/runtime-listing.xhtml | 45 +
.../client/webide/content/runtime-panel.js | 11 +
.../client/webide/content/runtimedetails.js | 153 +++
.../webide/content/runtimedetails.xhtml | 46 +
devtools/client/webide/content/simulator.js | 352 +++++
.../client/webide/content/simulator.xhtml | 99 ++
devtools/client/webide/content/webide.js | 1157 +++++++++++++++++
devtools/client/webide/content/webide.xul | 178 +++
devtools/client/webide/content/wifi-auth.js | 44 +
.../client/webide/content/wifi-auth.xhtml | 45 +
devtools/client/webide/modules/addons.js | 197 +++
devtools/client/webide/modules/app-manager.js | 850 ++++++++++++
.../client/webide/modules/app-projects.js | 235 ++++
.../client/webide/modules/app-validator.js | 292 +++++
devtools/client/webide/modules/build.js | 199 +++
devtools/client/webide/modules/config-view.js | 373 ++++++
devtools/client/webide/modules/moz.build | 21 +
.../client/webide/modules/project-list.js | 375 ++++++
.../client/webide/modules/runtime-list.js | 207 +++
devtools/client/webide/modules/runtimes.js | 673 ++++++++++
.../webide/modules/simulator-process.js | 325 +++++
devtools/client/webide/modules/simulators.js | 368 ++++++
devtools/client/webide/modules/tab-store.js | 178 +++
devtools/client/webide/modules/utils.js | 68 +
devtools/client/webide/moz.build | 23 +
devtools/client/webide/test/.eslintrc.js | 6 +
.../webide/test/addons/adbhelper-linux.xpi | Bin 0 -> 1293 bytes
.../webide/test/addons/adbhelper-linux64.xpi | Bin 0 -> 1293 bytes
.../webide/test/addons/adbhelper-mac64.xpi | Bin 0 -> 1293 bytes
.../webide/test/addons/adbhelper-win32.xpi | Bin 0 -> 1293 bytes
.../test/addons/fxdt-adapters-linux32.xpi | Bin 0 -> 1156 bytes
.../test/addons/fxdt-adapters-linux64.xpi | Bin 0 -> 1156 bytes
.../test/addons/fxdt-adapters-mac64.xpi | Bin 0 -> 1156 bytes
.../test/addons/fxdt-adapters-win32.xpi | Bin 0 -> 1156 bytes
.../test/addons/fxos_1_0_simulator-linux.xpi | Bin 0 -> 5046 bytes
.../addons/fxos_1_0_simulator-linux64.xpi | Bin 0 -> 5046 bytes
.../test/addons/fxos_1_0_simulator-mac64.xpi | Bin 0 -> 5044 bytes
.../test/addons/fxos_1_0_simulator-win32.xpi | Bin 0 -> 5046 bytes
.../test/addons/fxos_2_0_simulator-linux.xpi | Bin 0 -> 5046 bytes
.../addons/fxos_2_0_simulator-linux64.xpi | Bin 0 -> 5046 bytes
.../test/addons/fxos_2_0_simulator-mac64.xpi | Bin 0 -> 5043 bytes
.../test/addons/fxos_2_0_simulator-win32.xpi | Bin 0 -> 5045 bytes
.../test/addons/fxos_3_0_simulator-linux.xpi | Bin 0 -> 5045 bytes
.../addons/fxos_3_0_simulator-linux64.xpi | Bin 0 -> 5048 bytes
.../test/addons/fxos_3_0_simulator-mac64.xpi | Bin 0 -> 5048 bytes
.../test/addons/fxos_3_0_simulator-win32.xpi | Bin 0 -> 5044 bytes
.../addons/fxos_3_0_tv_simulator-linux.xpi | Bin 0 -> 5052 bytes
.../addons/fxos_3_0_tv_simulator-linux64.xpi | Bin 0 -> 5055 bytes
.../addons/fxos_3_0_tv_simulator-mac64.xpi | Bin 0 -> 5051 bytes
.../addons/fxos_3_0_tv_simulator-win32.xpi | Bin 0 -> 5051 bytes
.../client/webide/test/addons/simulators.json | 4 +
devtools/client/webide/test/app.zip | Bin 0 -> 480 bytes
devtools/client/webide/test/app/index.html | 6 +
.../client/webide/test/app/manifest.webapp | 5 +
devtools/client/webide/test/browser.ini | 12 +
devtools/client/webide/test/browser_tabs.js | 84 ++
devtools/client/webide/test/browser_widget.js | 15 +
.../webide/test/build_app1/package.json | 5 +
.../webide/test/build_app2/manifest.webapp | 1 +
.../webide/test/build_app2/package.json | 10 +
.../test/build_app2/stage/empty-directory | 0
.../test/build_app_windows1/package.json | 5 +
.../test/build_app_windows2/manifest.webapp | 1 +
.../test/build_app_windows2/package.json | 10 +
.../build_app_windows2/stage/empty-directory | 0
devtools/client/webide/test/chrome.ini | 71 +
.../client/webide/test/device_front_shared.js | 219 ++++
devtools/client/webide/test/doc_tabs.html | 15 +
devtools/client/webide/test/head.js | 248 ++++
.../client/webide/test/hosted_app.manifest | 3 +
devtools/client/webide/test/templates.json | 14 +
devtools/client/webide/test/test_addons.html | 176 +++
.../webide/test/test_app_validator.html | 205 +++
.../webide/test/test_autoconnect_runtime.html | 94 ++
.../webide/test/test_autoselect_project.html | 110 ++
devtools/client/webide/test/test_basic.html | 55 +
devtools/client/webide/test/test_build.html | 128 ++
.../webide/test/test_device_permissions.html | 81 ++
.../webide/test/test_device_preferences.html | 87 ++
.../webide/test/test_device_runtime.html | 81 ++
.../webide/test/test_device_settings.html | 87 ++
.../webide/test/test_duplicate_import.html | 77 ++
.../webide/test/test_fullscreenToolbox.html | 67 +
devtools/client/webide/test/test_import.html | 82 ++
.../webide/test/test_manifestUpdate.html | 98 ++
devtools/client/webide/test/test_newapp.html | 46 +
devtools/client/webide/test/test_runtime.html | 203 +++
.../client/webide/test/test_simulators.html | 426 ++++++
.../client/webide/test/test_telemetry.html | 325 +++++
devtools/client/webide/test/test_toolbox.html | 93 ++
devtools/client/webide/test/test_zoom.html | 77 ++
.../test/validator/no-name-or-icon/home.html | 0
.../validator/no-name-or-icon/manifest.webapp | 3 +
.../non-absolute-path/manifest.webapp | 7 +
.../validator/valid/alsoValid/manifest.webapp | 7 +
.../webide/test/validator/valid/home.html | 0
.../webide/test/validator/valid/icon.png | 0
.../test/validator/valid/manifest.webapp | 7 +
.../test/validator/wrong-launch-path/icon.png | 0
.../wrong-launch-path/manifest.webapp | 7 +
devtools/client/webide/themes/addons.css | 79 ++
devtools/client/webide/themes/config-view.css | 80 ++
devtools/client/webide/themes/deck.css | 91 ++
.../client/webide/themes/default-app-icon.png | Bin 0 -> 5208 bytes
devtools/client/webide/themes/details.css | 138 ++
devtools/client/webide/themes/icons.png | Bin 0 -> 35353 bytes
devtools/client/webide/themes/jar.mn | 24 +
devtools/client/webide/themes/logs.css | 18 +
devtools/client/webide/themes/monitor.css | 86 ++
devtools/client/webide/themes/moz.build | 7 +
devtools/client/webide/themes/newapp.css | 54 +
devtools/client/webide/themes/noise.png | Bin 0 -> 6216 bytes
.../client/webide/themes/panel-listing.css | 150 +++
.../client/webide/themes/permissionstable.css | 23 +
devtools/client/webide/themes/rocket.svg | 12 +
.../client/webide/themes/runtimedetails.css | 25 +
devtools/client/webide/themes/simulator.css | 41 +
devtools/client/webide/themes/throbber.svg | 22 +
devtools/client/webide/themes/webide.css | 149 +++
devtools/client/webide/themes/wifi-auth.css | 64 +
devtools/client/webide/webide-prefs.js | 35 +
163 files changed, 13812 insertions(+)
create mode 100644 devtools/client/locales/en-US/webide.dtd
create mode 100644 devtools/client/locales/en-US/webide.properties
create mode 100644 devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js
create mode 100644 devtools/client/projecteditor/lib/plugins/app-manager/moz.build
create mode 100644 devtools/client/projecteditor/lib/plugins/app-manager/plugin.js
create mode 100644 devtools/client/webide/components/moz.build
create mode 100644 devtools/client/webide/components/webideCli.js
create mode 100644 devtools/client/webide/components/webideComponents.manifest
create mode 100644 devtools/client/webide/content/addons.js
create mode 100644 devtools/client/webide/content/addons.xhtml
create mode 100644 devtools/client/webide/content/details.js
create mode 100644 devtools/client/webide/content/details.xhtml
create mode 100644 devtools/client/webide/content/devicepreferences.js
create mode 100644 devtools/client/webide/content/devicepreferences.xhtml
create mode 100644 devtools/client/webide/content/devicesettings.js
create mode 100644 devtools/client/webide/content/devicesettings.xhtml
create mode 100644 devtools/client/webide/content/jar.mn
create mode 100644 devtools/client/webide/content/logs.js
create mode 100644 devtools/client/webide/content/logs.xhtml
create mode 100644 devtools/client/webide/content/monitor.js
create mode 100644 devtools/client/webide/content/monitor.xhtml
create mode 100644 devtools/client/webide/content/moz.build
create mode 100644 devtools/client/webide/content/newapp.js
create mode 100644 devtools/client/webide/content/newapp.xul
create mode 100644 devtools/client/webide/content/permissionstable.js
create mode 100644 devtools/client/webide/content/permissionstable.xhtml
create mode 100644 devtools/client/webide/content/prefs.js
create mode 100644 devtools/client/webide/content/prefs.xhtml
create mode 100644 devtools/client/webide/content/project-listing.js
create mode 100644 devtools/client/webide/content/project-listing.xhtml
create mode 100644 devtools/client/webide/content/project-panel.js
create mode 100644 devtools/client/webide/content/runtime-listing.js
create mode 100644 devtools/client/webide/content/runtime-listing.xhtml
create mode 100644 devtools/client/webide/content/runtime-panel.js
create mode 100644 devtools/client/webide/content/runtimedetails.js
create mode 100644 devtools/client/webide/content/runtimedetails.xhtml
create mode 100644 devtools/client/webide/content/simulator.js
create mode 100644 devtools/client/webide/content/simulator.xhtml
create mode 100644 devtools/client/webide/content/webide.js
create mode 100644 devtools/client/webide/content/webide.xul
create mode 100644 devtools/client/webide/content/wifi-auth.js
create mode 100644 devtools/client/webide/content/wifi-auth.xhtml
create mode 100644 devtools/client/webide/modules/addons.js
create mode 100644 devtools/client/webide/modules/app-manager.js
create mode 100644 devtools/client/webide/modules/app-projects.js
create mode 100644 devtools/client/webide/modules/app-validator.js
create mode 100644 devtools/client/webide/modules/build.js
create mode 100644 devtools/client/webide/modules/config-view.js
create mode 100644 devtools/client/webide/modules/moz.build
create mode 100644 devtools/client/webide/modules/project-list.js
create mode 100644 devtools/client/webide/modules/runtime-list.js
create mode 100644 devtools/client/webide/modules/runtimes.js
create mode 100644 devtools/client/webide/modules/simulator-process.js
create mode 100644 devtools/client/webide/modules/simulators.js
create mode 100644 devtools/client/webide/modules/tab-store.js
create mode 100644 devtools/client/webide/modules/utils.js
create mode 100644 devtools/client/webide/moz.build
create mode 100644 devtools/client/webide/test/.eslintrc.js
create mode 100644 devtools/client/webide/test/addons/adbhelper-linux.xpi
create mode 100644 devtools/client/webide/test/addons/adbhelper-linux64.xpi
create mode 100644 devtools/client/webide/test/addons/adbhelper-mac64.xpi
create mode 100644 devtools/client/webide/test/addons/adbhelper-win32.xpi
create mode 100644 devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi
create mode 100644 devtools/client/webide/test/addons/fxdt-adapters-linux64.xpi
create mode 100644 devtools/client/webide/test/addons/fxdt-adapters-mac64.xpi
create mode 100644 devtools/client/webide/test/addons/fxdt-adapters-win32.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_1_0_simulator-linux.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_1_0_simulator-win32.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_2_0_simulator-win32.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_simulator-linux.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_simulator-linux64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_simulator-mac64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_simulator-win32.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_tv_simulator-linux64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_tv_simulator-mac64.xpi
create mode 100644 devtools/client/webide/test/addons/fxos_3_0_tv_simulator-win32.xpi
create mode 100644 devtools/client/webide/test/addons/simulators.json
create mode 100644 devtools/client/webide/test/app.zip
create mode 100644 devtools/client/webide/test/app/index.html
create mode 100644 devtools/client/webide/test/app/manifest.webapp
create mode 100644 devtools/client/webide/test/browser.ini
create mode 100644 devtools/client/webide/test/browser_tabs.js
create mode 100644 devtools/client/webide/test/browser_widget.js
create mode 100644 devtools/client/webide/test/build_app1/package.json
create mode 100644 devtools/client/webide/test/build_app2/manifest.webapp
create mode 100644 devtools/client/webide/test/build_app2/package.json
create mode 100644 devtools/client/webide/test/build_app2/stage/empty-directory
create mode 100644 devtools/client/webide/test/build_app_windows1/package.json
create mode 100644 devtools/client/webide/test/build_app_windows2/manifest.webapp
create mode 100644 devtools/client/webide/test/build_app_windows2/package.json
create mode 100644 devtools/client/webide/test/build_app_windows2/stage/empty-directory
create mode 100644 devtools/client/webide/test/chrome.ini
create mode 100644 devtools/client/webide/test/device_front_shared.js
create mode 100644 devtools/client/webide/test/doc_tabs.html
create mode 100644 devtools/client/webide/test/head.js
create mode 100644 devtools/client/webide/test/hosted_app.manifest
create mode 100644 devtools/client/webide/test/templates.json
create mode 100644 devtools/client/webide/test/test_addons.html
create mode 100644 devtools/client/webide/test/test_app_validator.html
create mode 100644 devtools/client/webide/test/test_autoconnect_runtime.html
create mode 100644 devtools/client/webide/test/test_autoselect_project.html
create mode 100644 devtools/client/webide/test/test_basic.html
create mode 100644 devtools/client/webide/test/test_build.html
create mode 100644 devtools/client/webide/test/test_device_permissions.html
create mode 100644 devtools/client/webide/test/test_device_preferences.html
create mode 100644 devtools/client/webide/test/test_device_runtime.html
create mode 100644 devtools/client/webide/test/test_device_settings.html
create mode 100644 devtools/client/webide/test/test_duplicate_import.html
create mode 100644 devtools/client/webide/test/test_fullscreenToolbox.html
create mode 100644 devtools/client/webide/test/test_import.html
create mode 100644 devtools/client/webide/test/test_manifestUpdate.html
create mode 100644 devtools/client/webide/test/test_newapp.html
create mode 100644 devtools/client/webide/test/test_runtime.html
create mode 100644 devtools/client/webide/test/test_simulators.html
create mode 100644 devtools/client/webide/test/test_telemetry.html
create mode 100644 devtools/client/webide/test/test_toolbox.html
create mode 100644 devtools/client/webide/test/test_zoom.html
create mode 100644 devtools/client/webide/test/validator/no-name-or-icon/home.html
create mode 100644 devtools/client/webide/test/validator/no-name-or-icon/manifest.webapp
create mode 100644 devtools/client/webide/test/validator/non-absolute-path/manifest.webapp
create mode 100644 devtools/client/webide/test/validator/valid/alsoValid/manifest.webapp
create mode 100644 devtools/client/webide/test/validator/valid/home.html
create mode 100644 devtools/client/webide/test/validator/valid/icon.png
create mode 100644 devtools/client/webide/test/validator/valid/manifest.webapp
create mode 100644 devtools/client/webide/test/validator/wrong-launch-path/icon.png
create mode 100644 devtools/client/webide/test/validator/wrong-launch-path/manifest.webapp
create mode 100644 devtools/client/webide/themes/addons.css
create mode 100644 devtools/client/webide/themes/config-view.css
create mode 100644 devtools/client/webide/themes/deck.css
create mode 100644 devtools/client/webide/themes/default-app-icon.png
create mode 100644 devtools/client/webide/themes/details.css
create mode 100644 devtools/client/webide/themes/icons.png
create mode 100644 devtools/client/webide/themes/jar.mn
create mode 100644 devtools/client/webide/themes/logs.css
create mode 100644 devtools/client/webide/themes/monitor.css
create mode 100644 devtools/client/webide/themes/moz.build
create mode 100644 devtools/client/webide/themes/newapp.css
create mode 100644 devtools/client/webide/themes/noise.png
create mode 100644 devtools/client/webide/themes/panel-listing.css
create mode 100644 devtools/client/webide/themes/permissionstable.css
create mode 100644 devtools/client/webide/themes/rocket.svg
create mode 100644 devtools/client/webide/themes/runtimedetails.css
create mode 100644 devtools/client/webide/themes/simulator.css
create mode 100644 devtools/client/webide/themes/throbber.svg
create mode 100644 devtools/client/webide/themes/webide.css
create mode 100644 devtools/client/webide/themes/wifi-auth.css
create mode 100644 devtools/client/webide/webide-prefs.js
diff --git a/application/basilisk/components/customizableui/CustomizableUI.jsm b/application/basilisk/components/customizableui/CustomizableUI.jsm
index cb92639619..81c49876ab 100644
--- a/application/basilisk/components/customizableui/CustomizableUI.jsm
+++ b/application/basilisk/components/customizableui/CustomizableUI.jsm
@@ -38,6 +38,7 @@ const kPrefCustomizationState = "browser.uiCustomization.state";
const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
+const kPrefWebIDEInNavbar = "devtools.webide.widget.inNavbarByDefault";
const kExpectedWindowURL = "chrome://browser/content/browser.xul";
@@ -223,6 +224,10 @@ var CustomizableUIInternal = {
"home-button",
];
+ if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
+ navbarPlacements.push("webide-button");
+ }
+
// Place this last, when createWidget is called for pocket, it will
// append to the toolbar.
if (Services.prefs.getPrefType("extensions.pocket.enabled") != Services.prefs.PREF_INVALID &&
diff --git a/devtools/client/framework/devtools-browser.js b/devtools/client/framework/devtools-browser.js
index 83a888ab99..5943743c41 100644
--- a/devtools/client/framework/devtools-browser.js
+++ b/devtools/client/framework/devtools-browser.js
@@ -123,6 +123,23 @@ var gDevToolsBrowser = exports.gDevToolsBrowser = {
win.DeveloperToolbar.show(false).catch(console.error);
}
+ // Enable WebIDE?
+ let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
+ idEls = [
+ "appmenu_webide",
+ "menu_webide"
+ ];
+ idEls.forEach(function (idEl) {
+ toggleMenuItem(idEl, webIDEEnabled);
+ });
+
+ let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
+ if (webIDEEnabled && showWebIDEWidget) {
+ gDevToolsBrowser.installWebIDEWidget();
+ } else {
+ gDevToolsBrowser.uninstallWebIDEWidget();
+ }
+
// Enable Browser Toolbox?
let chromeEnabled = Services.prefs.getBoolPref("devtools.chrome.enabled");
let devtoolsRemoteEnabled = Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
diff --git a/devtools/client/framework/gDevTools.jsm b/devtools/client/framework/gDevTools.jsm
index 6e0dc5e836..d825c0eaa3 100644
--- a/devtools/client/framework/gDevTools.jsm
+++ b/devtools/client/framework/gDevTools.jsm
@@ -126,9 +126,16 @@ let gDevToolsBrowserMethods = [
// Used by browser-sets.inc, command
"openConnectScreen",
+ // Used by browser-sets.inc, command
+ // itself, webide widget
+ "openWebIDE",
+
// Used by browser-sets.inc, command
"openContentProcessToolbox",
+ // Used by webide.js
+ "moveWebIDEWidgetInNavbar",
+
// Used by browser.js
"registerBrowserWindow",
@@ -139,6 +146,10 @@ let gDevToolsBrowserMethods = [
"forgetBrowserWindow"
];
this.gDevToolsBrowser = {
+ // Used by webide.js
+ get isWebIDEInitialized() {
+ return browser.isWebIDEInitialized;
+ },
// Used by a test (should be removed)
get _trackedBrowserWindows() {
return browser._trackedBrowserWindows;
diff --git a/devtools/client/locales/en-US/menus.properties b/devtools/client/locales/en-US/menus.properties
index 7030fe17d2..66e158cbd7 100644
--- a/devtools/client/locales/en-US/menus.properties
+++ b/devtools/client/locales/en-US/menus.properties
@@ -54,6 +54,11 @@ devToolbarMenu.accesskey = v
devToolbarMenu.key = VK_F2
devToolbarMenu.keytext = F2
+webide.label = WebIDE
+webide.accesskey = W
+webide.key = VK_F8
+webide.keytext = F8
+
devToolboxMenuItem.label = Toggle Tools
devToolboxMenuItem.accesskey = T
devToolboxMenuItem.key = I
diff --git a/devtools/client/locales/en-US/webide.dtd b/devtools/client/locales/en-US/webide.dtd
new file mode 100644
index 0000000000..554488f6d0
--- /dev/null
+++ b/devtools/client/locales/en-US/webide.dtd
@@ -0,0 +1,222 @@
+
+
+
+ %brandDTD;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/locales/en-US/webide.properties b/devtools/client/locales/en-US/webide.properties
new file mode 100644
index 0000000000..1540949063
--- /dev/null
+++ b/devtools/client/locales/en-US/webide.properties
@@ -0,0 +1,92 @@
+# 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/.
+
+title_noApp=WebIDE
+title_app=WebIDE: %S
+
+runtimeButton_label=Select Runtime
+projectButton_label=Open App
+
+mainProcess_label=Main Process
+
+local_runtime=Local Runtime
+remote_runtime=Remote Runtime
+remote_runtime_promptTitle=Remote Runtime
+remote_runtime_promptMessage=hostname:port
+
+importPackagedApp_title=Select Directory
+importHostedApp_title=Open Hosted App
+importHostedApp_header=Enter Manifest URL
+
+selectCustomBinary_title=Select custom B2G binary
+selectCustomProfile_title=Select custom Gaia profile
+
+notification_showTroubleShooting_label=Troubleshooting
+notification_showTroubleShooting_accesskey=T
+
+# LOCALIZATION NOTE (project_tab_loading): This is shown as a temporary tab
+# title for browser tab projects when the tab is still loading.
+project_tab_loading=Loading…
+
+# These messages appear in a notification box when an error occur.
+
+error_cantInstallNotFullyConnected=Can’t install project. Not fully connected.
+error_cantInstallValidationErrors=Can’t install project. Validation errors.
+error_listRunningApps=Can’t get app list from device
+
+# Variable: name of the operation (in english)
+error_operationTimeout=Operation timed out: %1$S
+error_operationFail=Operation failed: %1$S
+
+# Variable: app name
+error_cantConnectToApp=Can’t connect to app: %1$S
+
+# Variable: error message (in english)
+error_cantFetchAddonsJSON=Can’t fetch the add-on list: %S
+
+error_appProjectsLoadFailed=Unable to load project list. This can occur if you’ve used this profile with a newer version of the browser.
+error_folderCreationFailed=Unable to create project folder in the selected directory.
+
+# Variable: runtime app build ID (looks like this %Y%M%D format) and firefox build ID (same format)
+error_runtimeVersionTooRecent=The connected runtime has a more recent build date (%1$S) than your desktop browser (%2$S) does. This is an unsupported setup and may cause DevTools to fail. Please update the browser.
+
+addons_stable=stable
+addons_unstable=unstable
+# LOCALIZATION NOTE (addons_simulator_label): This label is shown as the name of
+# a given simulator version in the "Manage Simulators" pane. %1$S: Firefox OS
+# version in the simulator, ex. 1.3. %2$S: Simulator stability label, ex.
+# "stable" or "unstable".
+addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
+addons_install_button=install
+addons_uninstall_button=uninstall
+addons_adb_label=ADB Helper Add-on
+addons_adapters_label=Tools Adapters Add-on
+addons_adb_warning=USB devices won’t be detected without this add-on
+addons_status_unknown=?
+addons_status_installed=Installed
+addons_status_uninstalled=Not Installed
+addons_status_preparing=preparing
+addons_status_downloading=downloading
+addons_status_installing=installing
+
+runtimedetails_checkno=no
+runtimedetails_checkyes=yes
+runtimedetails_checkunknown=unknown (requires ADB Helper 0.4.0 or later)
+runtimedetails_notUSBDevice=Not a USB device
+
+# Validation status
+status_tooltip=Validation status: %1$S
+status_valid=VALID
+status_warning=WARNINGS
+status_error=ERRORS
+status_unknown=UNKNOWN
+
+# Device preferences and settings
+device_reset_default=Reset to default
+
+# Simulator options
+simulator_custom_device=Custom
+simulator_custom_binary=Custom B2G binary…
+simulator_custom_profile=Custom Gaia profile…
+simulator_default_profile=Use default
diff --git a/devtools/client/menus.js b/devtools/client/menus.js
index 23f024f04a..dbacb367da 100644
--- a/devtools/client/menus.js
+++ b/devtools/client/menus.js
@@ -86,6 +86,17 @@ exports.menuitems = [
},
checkbox: true
},
+ { id: "menu_webide",
+ l10nKey: "webide",
+ disabled: true,
+ oncommand() {
+ gDevToolsBrowser.openWebIDE();
+ },
+ key: {
+ id: "webide",
+ modifiers: "shift"
+ }
+ },
{ id: "menu_browserToolbox",
l10nKey: "browserToolboxMenu",
disabled: true,
diff --git a/devtools/client/moz.build b/devtools/client/moz.build
index 9699ec7261..b55aa5380e 100644
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -34,6 +34,7 @@ DIRS += [
'themes',
'webaudioeditor',
'webconsole',
+ 'webide',
]
# Shim old theme paths used by DevTools add-ons
diff --git a/devtools/client/preferences/devtools.js b/devtools/client/preferences/devtools.js
index e817b3a18b..7e0f5865d5 100644
--- a/devtools/client/preferences/devtools.js
+++ b/devtools/client/preferences/devtools.js
@@ -21,6 +21,9 @@ pref("devtools.loader.hotreload", false);
pref("devtools.toolbar.enabled", true);
pref("devtools.toolbar.visible", false);
+// Enable DevTools WebIDE by default
+pref("devtools.webide.enabled", true);
+
// Toolbox preferences
pref("devtools.toolbox.footer.height", 250);
pref("devtools.toolbox.sidebar.width", 500);
diff --git a/devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js b/devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js
new file mode 100644
index 0000000000..9a66770b0d
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/app-manager/app-project-editor.js
@@ -0,0 +1,56 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const promise = require("promise");
+const { ItchEditor } = require("devtools/client/projecteditor/lib/editors");
+
+var AppProjectEditor = Class({
+ extends: ItchEditor,
+
+ hidesToolbar: true,
+
+ initialize: function (host) {
+ ItchEditor.prototype.initialize.apply(this, arguments);
+ this.appended = promise.resolve();
+ this.host = host;
+ this.label = "app-manager";
+ },
+
+ destroy: function () {
+ this.elt.remove();
+ this.elt = null;
+ },
+
+ load: function (resource) {
+ let {appManagerOpts} = this.host.project;
+
+ // Only load the frame the first time it is selected
+ if (!this.iframe || this.iframe.getAttribute("src") !== appManagerOpts.projectOverviewURL) {
+
+ this.elt.textContent = "";
+ let iframe = this.iframe = this.elt.ownerDocument.createElement("iframe");
+ let iframeLoaded = this.iframeLoaded = promise.defer();
+
+ iframe.addEventListener("load", function onLoad() {
+ iframe.removeEventListener("load", onLoad);
+ iframeLoaded.resolve();
+ });
+
+ iframe.setAttribute("flex", "1");
+ iframe.setAttribute("src", appManagerOpts.projectOverviewURL);
+ this.elt.appendChild(iframe);
+
+ }
+
+ promise.all([this.iframeLoaded.promise, this.appended]).then(() => {
+ this.emit("load");
+ });
+ }
+});
+
+exports.AppProjectEditor = AppProjectEditor;
diff --git a/devtools/client/projecteditor/lib/plugins/app-manager/moz.build b/devtools/client/projecteditor/lib/plugins/app-manager/moz.build
new file mode 100644
index 0000000000..8aae527250
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/app-manager/moz.build
@@ -0,0 +1,10 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'app-project-editor.js',
+ 'plugin.js',
+)
diff --git a/devtools/client/projecteditor/lib/plugins/app-manager/plugin.js b/devtools/client/projecteditor/lib/plugins/app-manager/plugin.js
new file mode 100644
index 0000000000..82bbab34bd
--- /dev/null
+++ b/devtools/client/projecteditor/lib/plugins/app-manager/plugin.js
@@ -0,0 +1,77 @@
+const { Cu } = require("chrome");
+const { Class } = require("sdk/core/heritage");
+const { EventTarget } = require("sdk/event/target");
+const { emit } = require("sdk/event/core");
+const promise = require("promise");
+var { registerPlugin, Plugin } = require("devtools/client/projecteditor/lib/plugins/core");
+const { AppProjectEditor } = require("./app-project-editor");
+const OPTION_URL = "chrome://devtools/skin/images/tool-options.svg";
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var AppManagerRenderer = Class({
+ extends: Plugin,
+
+ isAppManagerProject: function () {
+ return !!this.host.project.appManagerOpts;
+ },
+ editorForResource: function (resource) {
+ if (!resource.parent && this.isAppManagerProject()) {
+ return AppProjectEditor;
+ }
+ },
+ getUI: function (parent) {
+ let doc = parent.ownerDocument;
+ if (parent.childElementCount == 0) {
+ let image = doc.createElement("image");
+ let optionImage = doc.createElement("image");
+ let flexElement = doc.createElement("div");
+ let nameLabel = doc.createElement("span");
+ let statusElement = doc.createElement("div");
+
+ image.className = "project-image";
+ optionImage.className = "project-options";
+ optionImage.setAttribute("src", OPTION_URL);
+ nameLabel.className = "project-name-label";
+ statusElement.className = "project-status";
+ flexElement.className = "project-flex";
+
+ parent.appendChild(image);
+ parent.appendChild(nameLabel);
+ parent.appendChild(flexElement);
+ parent.appendChild(statusElement);
+ parent.appendChild(optionImage);
+ }
+
+ return {
+ image: parent.querySelector(".project-image"),
+ nameLabel: parent.querySelector(".project-name-label"),
+ statusElement: parent.querySelector(".project-status")
+ };
+ },
+ onAnnotate: function (resource, editor, elt) {
+ if (resource.parent || !this.isAppManagerProject()) {
+ return;
+ }
+
+ let {appManagerOpts} = this.host.project;
+ let doc = elt.ownerDocument;
+
+ let {image, nameLabel, statusElement} = this.getUI(elt);
+ let name = appManagerOpts.name || resource.basename;
+ let url = appManagerOpts.iconUrl || "icon-sample.png";
+ let status = appManagerOpts.validationStatus || "unknown";
+ let tooltip = Strings.formatStringFromName("status_tooltip",
+ [Strings.GetStringFromName("status_" + status)], 1);
+
+ nameLabel.textContent = name;
+ image.setAttribute("src", url);
+ statusElement.setAttribute("status", status);
+ statusElement.setAttribute("tooltiptext", tooltip);
+
+ return true;
+ }
+});
+
+exports.AppManagerRenderer = AppManagerRenderer;
+registerPlugin(AppManagerRenderer);
diff --git a/devtools/client/projecteditor/lib/plugins/moz.build b/devtools/client/projecteditor/lib/plugins/moz.build
index 99d864e136..17bff7ce04 100644
--- a/devtools/client/projecteditor/lib/plugins/moz.build
+++ b/devtools/client/projecteditor/lib/plugins/moz.build
@@ -5,6 +5,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
+ 'app-manager',
'delete',
'dirty',
'image-view',
diff --git a/devtools/client/projecteditor/lib/projecteditor.js b/devtools/client/projecteditor/lib/projecteditor.js
index 27127b0a03..a3ef062496 100644
--- a/devtools/client/projecteditor/lib/projecteditor.js
+++ b/devtools/client/projecteditor/lib/projecteditor.js
@@ -31,6 +31,7 @@ require("devtools/client/projecteditor/lib/plugins/new/new");
require("devtools/client/projecteditor/lib/plugins/rename/rename");
require("devtools/client/projecteditor/lib/plugins/save/save");
require("devtools/client/projecteditor/lib/plugins/image-view/plugin");
+require("devtools/client/projecteditor/lib/plugins/app-manager/plugin");
require("devtools/client/projecteditor/lib/plugins/status-bar/plugin");
// Uncomment to enable logging.
diff --git a/devtools/client/shared/telemetry.js b/devtools/client/shared/telemetry.js
index 547b1c07fe..38a21cef6f 100644
--- a/devtools/client/shared/telemetry.js
+++ b/devtools/client/shared/telemetry.js
@@ -177,6 +177,23 @@ Telemetry.prototype = {
histogram: "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
timerHistogram: "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS"
},
+ webide: {
+ histogram: "DEVTOOLS_WEBIDE_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBIDE_TIME_ACTIVE_SECONDS"
+ },
+ webideProjectEditor: {
+ histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_OPENED_COUNT",
+ timerHistogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_TIME_ACTIVE_SECONDS"
+ },
+ webideProjectEditorSave: {
+ histogram: "DEVTOOLS_WEBIDE_PROJECT_EDITOR_SAVE_COUNT",
+ },
+ webideNewProject: {
+ histogram: "DEVTOOLS_WEBIDE_NEW_PROJECT_COUNT",
+ },
+ webideImportProject: {
+ histogram: "DEVTOOLS_WEBIDE_IMPORT_PROJECT_COUNT",
+ },
custom: {
histogram: "DEVTOOLS_CUSTOM_OPENED_COUNT",
timerHistogram: "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS"
diff --git a/devtools/client/webide/components/moz.build b/devtools/client/webide/components/moz.build
new file mode 100644
index 0000000000..d4047c2956
--- /dev/null
+++ b/devtools/client/webide/components/moz.build
@@ -0,0 +1,10 @@
+# -*- 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/.
+
+EXTRA_COMPONENTS += [
+ 'webideCli.js',
+ 'webideComponents.manifest',
+]
diff --git a/devtools/client/webide/components/webideCli.js b/devtools/client/webide/components/webideCli.js
new file mode 100644
index 0000000000..0f75da2c4a
--- /dev/null
+++ b/devtools/client/webide/components/webideCli.js
@@ -0,0 +1,58 @@
+/* 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 Ci = Components.interfaces;
+const Cu = Components.utils;
+
+const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
+
+/**
+ * Handles --webide command line option.
+ */
+
+function webideCli() { }
+
+webideCli.prototype = {
+ handle: function (cmdLine) {
+ if (!cmdLine.handleFlag("webide", false)) {
+ return;
+ }
+
+ // If --webide is used remotely, we don't want to open
+ // a new tab.
+ //
+ // If --webide is used for a new Firefox instance, we
+ // want to open webide only.
+ cmdLine.preventDefault = true;
+
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.focus();
+ } else {
+ win = Services.ww.openWindow(null,
+ "chrome://webide/content/",
+ "webide",
+ "chrome,centerscreen,resizable,dialog=no",
+ null);
+ }
+
+ if (cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ // If this is a new Firefox instance, and because we will only start
+ // webide, we need to notify "sessionstore-windows-restored" to trigger
+ // addons registration (for simulators and adb helper).
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
+ }
+ },
+
+ helpInfo: "",
+
+ classID: Components.ID("{79b7b44e-de5e-4e4c-b7a2-044003c615d9}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([webideCli]);
diff --git a/devtools/client/webide/components/webideComponents.manifest b/devtools/client/webide/components/webideComponents.manifest
new file mode 100644
index 0000000000..03af9758cb
--- /dev/null
+++ b/devtools/client/webide/components/webideComponents.manifest
@@ -0,0 +1,4 @@
+# webide components
+component {79b7b44e-de5e-4e4c-b7a2-044003c615d9} webideCli.js
+contract @mozilla.org/browser/webide-clh;1 {79b7b44e-de5e-4e4c-b7a2-044003c615d9}
+category command-line-handler a-webide @mozilla.org/browser/webide-clh;1
diff --git a/devtools/client/webide/content/addons.js b/devtools/client/webide/content/addons.js
new file mode 100644
index 0000000000..3948b040f1
--- /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 0000000000..6f3bc1e7c9
--- /dev/null
+++ b/devtools/client/webide/content/addons.xhtml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+ &addons_title;
+
+
+
+
+
diff --git a/devtools/client/webide/content/details.js b/devtools/client/webide/content/details.js
new file mode 100644
index 0000000000..9097cd8c5f
--- /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 0000000000..a04c37b0cc
--- /dev/null
+++ b/devtools/client/webide/content/details.xhtml
@@ -0,0 +1,54 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &details_showPrepackageLog_button;
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/devicepreferences.js b/devtools/client/webide/content/devicepreferences.js
new file mode 100644
index 0000000000..14c020f120
--- /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 0000000000..dafb6f15ff
--- /dev/null
+++ b/devtools/client/webide/content/devicepreferences.xhtml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/devicesettings.js b/devtools/client/webide/content/devicesettings.js
new file mode 100644
index 0000000000..987df5995b
--- /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 0000000000..0406c6f076
--- /dev/null
+++ b/devtools/client/webide/content/devicesettings.xhtml
@@ -0,0 +1,50 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/jar.mn b/devtools/client/webide/content/jar.mn
new file mode 100644
index 0000000000..db79fdb513
--- /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 0000000000..157d83b676
--- /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 0000000000..8d003e509e
--- /dev/null
+++ b/devtools/client/webide/content/logs.xhtml
@@ -0,0 +1,33 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &logs_title;
+
+
+
+
+
diff --git a/devtools/client/webide/content/monitor.js b/devtools/client/webide/content/monitor.js
new file mode 100644
index 0000000000..a5d80d460e
--- /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 0000000000..552f3826c1
--- /dev/null
+++ b/devtools/client/webide/content/monitor.xhtml
@@ -0,0 +1,31 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &monitor_title;
+
+
+
diff --git a/devtools/client/webide/content/moz.build b/devtools/client/webide/content/moz.build
new file mode 100644
index 0000000000..aac3a838c4
--- /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 0000000000..d47bfabecd
--- /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 0000000000..7ff083519c
--- /dev/null
+++ b/devtools/client/webide/content/newapp.xul
@@ -0,0 +1,33 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+ &newAppLoadingTemplate;
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/permissionstable.js b/devtools/client/webide/content/permissionstable.js
new file mode 100644
index 0000000000..22c74bd0d7
--- /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 0000000000..361cfece88
--- /dev/null
+++ b/devtools/client/webide/content/permissionstable.xhtml
@@ -0,0 +1,36 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+ &permissionstable_title;
+
+
+
+ &permissionstable_name_header;
+ type:web
+ type:privileged
+ type:certified
+
+
+
+
diff --git a/devtools/client/webide/content/prefs.js b/devtools/client/webide/content/prefs.js
new file mode 100644
index 0000000000..75f6233ba6
--- /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 0000000000..726ca772cb
--- /dev/null
+++ b/devtools/client/webide/content/prefs.xhtml
@@ -0,0 +1,112 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+ &prefs_title;
+
+ &prefs_general_title;
+
+
+
+ &prefs_editor_title;
+
+
+
+
+
diff --git a/devtools/client/webide/content/project-listing.js b/devtools/client/webide/content/project-listing.js
new file mode 100644
index 0000000000..5641f6c0cb
--- /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 0000000000..337befe5d8
--- /dev/null
+++ b/devtools/client/webide/content/project-listing.xhtml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
&projectMenu_newApp_label;
+
&projectMenu_importPackagedApp_label;
+
&projectMenu_importHostedApp_label;
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/project-panel.js b/devtools/client/webide/content/project-panel.js
new file mode 100644
index 0000000000..54eab82518
--- /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 0000000000..0a1a40a2a0
--- /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 0000000000..f648fac12c
--- /dev/null
+++ b/devtools/client/webide/content/runtime-listing.xhtml
@@ -0,0 +1,45 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
&runtimePanel_nousbdevice;
+
&runtimePanel_noadbhelper;
+
+
+
+
+
+
&runtimePanel_installsimulator;
+
+
+
+ &runtimeMenu_showDetails_label;
+ &runtimeMenu_showPermissionTable_label;
+ &runtimeMenu_showDevicePrefs_label;
+ &runtimeMenu_showSettings_label;
+ &runtimeMenu_takeScreenshot_label;
+ &runtimeMenu_disconnect_label;
+
+
+
+
+
diff --git a/devtools/client/webide/content/runtime-panel.js b/devtools/client/webide/content/runtime-panel.js
new file mode 100644
index 0000000000..3646fa15cc
--- /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 0000000000..dea423e810
--- /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 0000000000..b2f74728ad
--- /dev/null
+++ b/devtools/client/webide/content/runtimedetails.xhtml
@@ -0,0 +1,46 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+ &runtimedetails_title;
+
+
+
+ &runtimedetails_adbIsRoot;
+
+ &runtimedetails_summonADBRoot;
+ &runtimedetails_ADBRootWarning;
+
+
+
+ &runtimedetails_unrestrictedPrivileges;
+
+ &runtimedetails_requestPrivileges;
+ &runtimedetails_privilegesWarning;
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/simulator.js b/devtools/client/webide/content/simulator.js
new file mode 100644
index 0000000000..ddc1cbed12
--- /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
+
+
+
+
+
diff --git a/devtools/client/webide/content/webide.js b/devtools/client/webide/content/webide.js
new file mode 100644
index 0000000000..c222332e3e
--- /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 0000000000..a3e4355b9a
--- /dev/null
+++ b/devtools/client/webide/content/webide.xul
@@ -0,0 +1,178 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/content/wifi-auth.js b/devtools/client/webide/content/wifi-auth.js
new file mode 100644
index 0000000000..5ae5d824c3
--- /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 0000000000..cfeec3c960
--- /dev/null
+++ b/devtools/client/webide/content/wifi-auth.xhtml
@@ -0,0 +1,45 @@
+
+
+
+
+
+ %webideDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ &wifi_auth_scan_request;
+
+
+
+
+
+
+
diff --git a/devtools/client/webide/modules/addons.js b/devtools/client/webide/modules/addons.js
new file mode 100644
index 0000000000..4dc09f1caf
--- /dev/null
+++ b/devtools/client/webide/modules/addons.js
@@ -0,0 +1,197 @@
+/* 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 promise = require("promise");
+const {AddonManager} = require("resource://gre/modules/AddonManager.jsm");
+const Services = require("Services");
+const {getJSON} = require("devtools/client/shared/getjson");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+const ADDONS_URL = "devtools.webide.addonsURL";
+
+var SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
+var ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
+var ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL");
+var SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
+var ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
+var ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID");
+
+var platform = Services.appShell.hiddenDOMWindow.navigator.platform;
+var OS = "";
+if (platform.indexOf("Win") != -1) {
+ OS = "win32";
+} else if (platform.indexOf("Mac") != -1) {
+ OS = "mac64";
+} else if (platform.indexOf("Linux") != -1) {
+ if (platform.indexOf("x86_64") != -1) {
+ OS = "linux64";
+ } else {
+ OS = "linux32";
+ }
+}
+
+var addonsListener = {};
+addonsListener.onEnabled =
+addonsListener.onDisabled =
+addonsListener.onInstalled =
+addonsListener.onUninstalled = (updatedAddon) => {
+ GetAvailableAddons().then(addons => {
+ for (let a of [...addons.simulators, addons.adb, addons.adapters]) {
+ if (a.addonID == updatedAddon.id) {
+ a.updateInstallStatus();
+ }
+ }
+ });
+};
+AddonManager.addAddonListener(addonsListener);
+
+var GetAvailableAddons_promise = null;
+var GetAvailableAddons = exports.GetAvailableAddons = function () {
+ if (!GetAvailableAddons_promise) {
+ let deferred = promise.defer();
+ GetAvailableAddons_promise = deferred.promise;
+ let addons = {
+ simulators: [],
+ adb: null
+ };
+ getJSON(ADDONS_URL).then(json => {
+ for (let stability in json) {
+ for (let version of json[stability]) {
+ addons.simulators.push(new SimulatorAddon(stability, version));
+ }
+ }
+ addons.adb = new ADBAddon();
+ addons.adapters = new AdaptersAddon();
+ deferred.resolve(addons);
+ }, e => {
+ GetAvailableAddons_promise = null;
+ deferred.reject(e);
+ });
+ }
+ return GetAvailableAddons_promise;
+};
+
+exports.ForgetAddonsList = function () {
+ GetAvailableAddons_promise = null;
+};
+
+function Addon() {}
+Addon.prototype = {
+ _status: "unknown",
+ set status(value) {
+ if (this._status != value) {
+ this._status = value;
+ this.emit("update");
+ }
+ },
+ get status() {
+ return this._status;
+ },
+
+ updateInstallStatus: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && !addon.userDisabled) {
+ this.status = "installed";
+ } else {
+ this.status = "uninstalled";
+ }
+ });
+ },
+
+ install: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ if (addon && !addon.userDisabled) {
+ this.status = "installed";
+ return;
+ }
+ this.status = "preparing";
+ if (addon && addon.userDisabled) {
+ addon.userDisabled = false;
+ } else {
+ AddonManager.getInstallForURL(this.xpiLink, (install) => {
+ install.addListener(this);
+ install.install();
+ }, "application/x-xpinstall");
+ }
+ });
+ },
+
+ uninstall: function () {
+ AddonManager.getAddonByID(this.addonID, (addon) => {
+ addon.uninstall();
+ });
+ },
+
+ installFailureHandler: function (install, message) {
+ this.status = "uninstalled";
+ this.emit("failure", message);
+ },
+
+ onDownloadStarted: function () {
+ this.status = "downloading";
+ },
+
+ onInstallStarted: function () {
+ this.status = "installing";
+ },
+
+ onDownloadProgress: function (install) {
+ if (install.maxProgress == -1) {
+ this.emit("progress", -1);
+ } else {
+ this.emit("progress", install.progress / install.maxProgress);
+ }
+ },
+
+ onInstallEnded: function ({addon}) {
+ addon.userDisabled = false;
+ },
+
+ onDownloadCancelled: function (install) {
+ this.installFailureHandler(install, "Download cancelled");
+ },
+ onDownloadFailed: function (install) {
+ this.installFailureHandler(install, "Download failed");
+ },
+ onInstallCancelled: function (install) {
+ this.installFailureHandler(install, "Install cancelled");
+ },
+ onInstallFailed: function (install) {
+ this.installFailureHandler(install, "Install failed");
+ },
+};
+
+function SimulatorAddon(stability, version) {
+ EventEmitter.decorate(this);
+ this.stability = stability;
+ this.version = version;
+ // This addon uses the string "linux" for "linux32"
+ let fixedOS = OS == "linux32" ? "linux" : OS;
+ this.xpiLink = SIMULATOR_LINK.replace(/#OS#/g, fixedOS)
+ .replace(/#VERSION#/g, version)
+ .replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.addonID = SIMULATOR_ADDON_ID.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
+ this.updateInstallStatus();
+}
+SimulatorAddon.prototype = Object.create(Addon.prototype);
+
+function ADBAddon() {
+ EventEmitter.decorate(this);
+ // This addon uses the string "linux" for "linux32"
+ let fixedOS = OS == "linux32" ? "linux" : OS;
+ this.xpiLink = ADB_LINK.replace(/#OS#/g, fixedOS);
+ this.addonID = ADB_ADDON_ID;
+ this.updateInstallStatus();
+}
+ADBAddon.prototype = Object.create(Addon.prototype);
+
+function AdaptersAddon() {
+ EventEmitter.decorate(this);
+ this.xpiLink = ADAPTERS_LINK.replace(/#OS#/g, OS);
+ this.addonID = ADAPTERS_ADDON_ID;
+ this.updateInstallStatus();
+}
+AdaptersAddon.prototype = Object.create(Addon.prototype);
diff --git a/devtools/client/webide/modules/app-manager.js b/devtools/client/webide/modules/app-manager.js
new file mode 100644
index 0000000000..88dfcdd443
--- /dev/null
+++ b/devtools/client/webide/modules/app-manager.js
@@ -0,0 +1,850 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cu} = require("chrome");
+
+const promise = require("promise");
+const {TargetFactory} = require("devtools/client/framework/target");
+const Services = require("Services");
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const EventEmitter = require("devtools/shared/event-emitter");
+const {TextEncoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const TabStore = require("devtools/client/webide/modules/tab-store");
+const {AppValidator} = require("devtools/client/webide/modules/app-validator");
+const {ConnectionManager, Connection} = require("devtools/shared/client/connection-manager");
+const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
+const {getDeviceFront} = require("devtools/shared/fronts/device");
+const {getPreferenceFront} = require("devtools/shared/fronts/preference");
+const {getSettingsFront} = require("devtools/shared/fronts/settings");
+const {Task} = require("devtools/shared/task");
+const {RuntimeScanners, RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const Telemetry = require("devtools/client/shared/telemetry");
+const {ProjectBuilding} = require("./build");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var AppManager = exports.AppManager = {
+
+ DEFAULT_PROJECT_ICON: "chrome://webide/skin/default-app-icon.png",
+ DEFAULT_PROJECT_NAME: "--",
+
+ _initialized: false,
+
+ init: function () {
+ if (this._initialized) {
+ return;
+ }
+ this._initialized = true;
+
+ let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
+ this.connection = ConnectionManager.createConnection("localhost", port);
+ this.onConnectionChanged = this.onConnectionChanged.bind(this);
+ this.connection.on(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
+
+ this.tabStore = new TabStore(this.connection);
+ this.onTabList = this.onTabList.bind(this);
+ this.onTabNavigate = this.onTabNavigate.bind(this);
+ this.onTabClosed = this.onTabClosed.bind(this);
+ this.tabStore.on("tab-list", this.onTabList);
+ this.tabStore.on("navigate", this.onTabNavigate);
+ this.tabStore.on("closed", this.onTabClosed);
+
+ this._clearRuntimeList();
+ this._rebuildRuntimeList = this._rebuildRuntimeList.bind(this);
+ RuntimeScanners.on("runtime-list-updated", this._rebuildRuntimeList);
+ RuntimeScanners.enable();
+ this._rebuildRuntimeList();
+
+ this.onInstallProgress = this.onInstallProgress.bind(this);
+
+ this._telemetry = new Telemetry();
+ },
+
+ destroy: function () {
+ if (!this._initialized) {
+ return;
+ }
+ this._initialized = false;
+
+ this.selectedProject = null;
+ this.selectedRuntime = null;
+ RuntimeScanners.off("runtime-list-updated", this._rebuildRuntimeList);
+ RuntimeScanners.disable();
+ this.runtimeList = null;
+ this.tabStore.off("tab-list", this.onTabList);
+ this.tabStore.off("navigate", this.onTabNavigate);
+ this.tabStore.off("closed", this.onTabClosed);
+ this.tabStore.destroy();
+ this.tabStore = null;
+ this.connection.off(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
+ this._listTabsResponse = null;
+ this.connection.disconnect();
+ this.connection = null;
+ },
+
+ /**
+ * This module emits various events when state changes occur. The basic event
+ * naming scheme is that event "X" means "X has changed" or "X is available".
+ * Some names are more detailed to clarify their precise meaning.
+ *
+ * The events this module may emit include:
+ * before-project:
+ * The selected project is about to change. The event includes a special
+ * |cancel| callback that will abort the project change if desired.
+ * connection:
+ * The connection status has changed (connected, disconnected, etc.)
+ * install-progress:
+ * A project being installed to a runtime has made further progress. This
+ * event contains additional details about exactly how far the process is
+ * when such information is available.
+ * project:
+ * The selected project has changed.
+ * project-started:
+ * The selected project started running on the connected runtime.
+ * project-stopped:
+ * The selected project stopped running on the connected runtime.
+ * project-removed:
+ * The selected project was removed from the project list.
+ * project-validated:
+ * The selected project just completed validation. As part of validation,
+ * many pieces of metadata about the project are refreshed, including its
+ * name, manifest details, etc.
+ * runtime:
+ * The selected runtime has changed.
+ * runtime-apps-icons:
+ * The list of URLs for the runtime app icons are available.
+ * runtime-global-actors:
+ * The list of global actors for the entire runtime (but not actors for a
+ * specific tab or app) are now available, so we can test for features
+ * like preferences and settings.
+ * runtime-details:
+ * The selected runtime's details have changed, such as its user-visible
+ * name.
+ * runtime-list:
+ * The list of available runtimes has changed, or any of the user-visible
+ * details (like names) for the non-selected runtimes has changed.
+ * runtime-telemetry:
+ * Detailed runtime telemetry has been recorded. Used by tests.
+ * runtime-targets:
+ * The list of remote runtime targets available from the currently
+ * connected runtime (such as tabs or apps) has changed, or any of the
+ * user-visible details (like names) for the non-selected runtime targets
+ * has changed. This event includes |type| in the details, to distinguish
+ * "apps" and "tabs".
+ */
+ update: function (what, details) {
+ // Anything we want to forward to the UI
+ this.emit("app-manager-update", what, details);
+ },
+
+ reportError: function (l10nProperty, ...l10nArgs) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (win) {
+ win.UI.reportError(l10nProperty, ...l10nArgs);
+ } else {
+ let text;
+ if (l10nArgs.length > 0) {
+ text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
+ } else {
+ text = Strings.GetStringFromName(l10nProperty);
+ }
+ console.error(text);
+ }
+ },
+
+ onConnectionChanged: function () {
+ console.log("Connection status changed: " + this.connection.status);
+
+ if (this.connection.status == Connection.Status.DISCONNECTED) {
+ this.selectedRuntime = null;
+ }
+
+ if (!this.connected) {
+ if (this._appsFront) {
+ this._appsFront.off("install-progress", this.onInstallProgress);
+ this._appsFront.unwatchApps();
+ this._appsFront = null;
+ }
+ this._listTabsResponse = null;
+ } else {
+ this.connection.client.listTabs((response) => {
+ if (response.webappsActor) {
+ let front = new AppActorFront(this.connection.client,
+ response);
+ front.on("install-progress", this.onInstallProgress);
+ front.watchApps(() => this.checkIfProjectIsRunning())
+ .then(() => {
+ // This can't be done earlier as many operations
+ // in the apps actor require watchApps to be called
+ // first.
+ this._appsFront = front;
+ this._listTabsResponse = response;
+ this._recordRuntimeInfo();
+ this.update("runtime-global-actors");
+ })
+ .then(() => {
+ this.checkIfProjectIsRunning();
+ this.update("runtime-targets", { type: "apps" });
+ front.fetchIcons().then(() => this.update("runtime-apps-icons"));
+ });
+ } else {
+ this._listTabsResponse = response;
+ this._recordRuntimeInfo();
+ this.update("runtime-global-actors");
+ }
+ });
+ }
+
+ this.update("connection");
+ },
+
+ get connected() {
+ return this.connection &&
+ this.connection.status == Connection.Status.CONNECTED;
+ },
+
+ get apps() {
+ if (this._appsFront) {
+ return this._appsFront.apps;
+ } else {
+ return new Map();
+ }
+ },
+
+ onInstallProgress: function (event, details) {
+ this.update("install-progress", details);
+ },
+
+ isProjectRunning: function () {
+ if (this.selectedProject.type == "mainProcess" ||
+ this.selectedProject.type == "tab") {
+ return true;
+ }
+
+ let app = this._getProjectFront(this.selectedProject);
+ return app && app.running;
+ },
+
+ checkIfProjectIsRunning: function () {
+ if (this.selectedProject) {
+ if (this.isProjectRunning()) {
+ this.update("project-started");
+ } else {
+ this.update("project-stopped");
+ }
+ }
+ },
+
+ listTabs: function () {
+ return this.tabStore.listTabs();
+ },
+
+ onTabList: function () {
+ this.update("runtime-targets", { type: "tabs" });
+ },
+
+ // TODO: Merge this into TabProject as part of project-agnostic work
+ onTabNavigate: function () {
+ this.update("runtime-targets", { type: "tabs" });
+ if (this.selectedProject.type !== "tab") {
+ return;
+ }
+ let tab = this.selectedProject.app = this.tabStore.selectedTab;
+ let uri = NetUtil.newURI(tab.url);
+ // Wanted to use nsIFaviconService here, but it only works for visited
+ // tabs, so that's no help for any remote tabs. Maybe some favicon wizard
+ // knows how to get high-res favicons easily, or we could offer actor
+ // support for this (bug 1061654).
+ tab.favicon = uri.prePath + "/favicon.ico";
+ tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
+ if (uri.scheme.startsWith("http")) {
+ tab.name = uri.host + ": " + tab.name;
+ }
+ this.selectedProject.location = tab.url;
+ this.selectedProject.name = tab.name;
+ this.selectedProject.icon = tab.favicon;
+ this.update("project-validated");
+ },
+
+ onTabClosed: function () {
+ if (this.selectedProject.type !== "tab") {
+ return;
+ }
+ this.selectedProject = null;
+ },
+
+ reloadTab: function () {
+ if (this.selectedProject && this.selectedProject.type != "tab") {
+ return promise.reject("tried to reload non-tab project");
+ }
+ return this.getTarget().then(target => {
+ target.activeTab.reload();
+ }, console.error.bind(console));
+ },
+
+ getTarget: function () {
+ if (this.selectedProject.type == "mainProcess") {
+ // Fx >=39 exposes a ChromeActor to debug the main process
+ if (this.connection.client.mainRoot.traits.allowChromeProcess) {
+ return this.connection.client.getProcess()
+ .then(aResponse => {
+ return TargetFactory.forRemoteTab({
+ form: aResponse.form,
+ client: this.connection.client,
+ chrome: true
+ });
+ });
+ } else {
+ // Fx <39 exposes tab actors on the root actor
+ return TargetFactory.forRemoteTab({
+ form: this._listTabsResponse,
+ client: this.connection.client,
+ chrome: true,
+ isTabActor: false
+ });
+ }
+ }
+
+ if (this.selectedProject.type == "tab") {
+ return this.tabStore.getTargetForTab();
+ }
+
+ let app = this._getProjectFront(this.selectedProject);
+ if (!app) {
+ return promise.reject("Can't find app front for selected project");
+ }
+
+ return Task.spawn(function* () {
+ // Once we asked the app to launch, the app isn't necessary completely loaded.
+ // launch request only ask the app to launch and immediatly returns.
+ // We have to keep trying to get app tab actors required to create its target.
+
+ for (let i = 0; i < 10; i++) {
+ try {
+ return yield app.getTarget();
+ } catch (e) {}
+ let deferred = promise.defer();
+ setTimeout(deferred.resolve, 500);
+ yield deferred.promise;
+ }
+
+ AppManager.reportError("error_cantConnectToApp", app.manifest.manifestURL);
+ throw new Error("can't connect to app");
+ });
+ },
+
+ getProjectManifestURL: function (project) {
+ let manifest = null;
+ if (project.type == "runtimeApp") {
+ manifest = project.app.manifestURL;
+ }
+
+ if (project.type == "hosted") {
+ manifest = project.location;
+ }
+
+ if (project.type == "packaged" && project.packagedAppOrigin) {
+ manifest = "app://" + project.packagedAppOrigin + "/manifest.webapp";
+ }
+
+ return manifest;
+ },
+
+ _getProjectFront: function (project) {
+ let manifest = this.getProjectManifestURL(project);
+ if (manifest && this._appsFront) {
+ return this._appsFront.apps.get(manifest);
+ }
+ return null;
+ },
+
+ _selectedProject: null,
+ set selectedProject(project) {
+ // A regular comparison doesn't work as we recreate a new object every time
+ let prev = this._selectedProject;
+ if (!prev && !project) {
+ return;
+ } else if (prev && project && prev.type === project.type) {
+ let type = project.type;
+ if (type === "runtimeApp") {
+ if (prev.app.manifestURL === project.app.manifestURL) {
+ return;
+ }
+ } else if (type === "tab") {
+ if (prev.app.actor === project.app.actor) {
+ return;
+ }
+ } else if (type === "packaged" || type === "hosted") {
+ if (prev.location === project.location) {
+ return;
+ }
+ } else if (type === "mainProcess") {
+ return;
+ } else {
+ throw new Error("Unsupported project type: " + type);
+ }
+ }
+
+ let cancelled = false;
+ this.update("before-project", { cancel: () => { cancelled = true; } });
+ if (cancelled) {
+ return;
+ }
+
+ this._selectedProject = project;
+
+ // Clear out tab store's selected state, if any
+ this.tabStore.selectedTab = null;
+
+ if (project) {
+ if (project.type == "packaged" ||
+ project.type == "hosted") {
+ this.validateAndUpdateProject(project);
+ }
+ if (project.type == "tab") {
+ this.tabStore.selectedTab = project.app;
+ }
+ }
+
+ this.update("project");
+ this.checkIfProjectIsRunning();
+ },
+ get selectedProject() {
+ return this._selectedProject;
+ },
+
+ removeSelectedProject: Task.async(function* () {
+ let location = this.selectedProject.location;
+ AppManager.selectedProject = null;
+ // If the user cancels the removeProject operation, don't remove the project
+ if (AppManager.selectedProject != null) {
+ return;
+ }
+
+ yield AppProjects.remove(location);
+ AppManager.update("project-removed");
+ }),
+
+ packageProject: Task.async(function* (project) {
+ if (!project) {
+ return;
+ }
+ if (project.type == "packaged" ||
+ project.type == "hosted") {
+ yield ProjectBuilding.build({
+ project: project,
+ logger: this.update.bind(this, "pre-package")
+ });
+ }
+ }),
+
+ _selectedRuntime: null,
+ set selectedRuntime(value) {
+ this._selectedRuntime = value;
+ if (!value && this.selectedProject &&
+ (this.selectedProject.type == "mainProcess" ||
+ this.selectedProject.type == "runtimeApp" ||
+ this.selectedProject.type == "tab")) {
+ this.selectedProject = null;
+ }
+ this.update("runtime");
+ },
+
+ get selectedRuntime() {
+ return this._selectedRuntime;
+ },
+
+ connectToRuntime: function (runtime) {
+
+ if (this.connected && this.selectedRuntime === runtime) {
+ // Already connected
+ return promise.resolve();
+ }
+
+ let deferred = promise.defer();
+
+ this.disconnectRuntime().then(() => {
+ this.selectedRuntime = runtime;
+
+ let onConnectedOrDisconnected = () => {
+ this.connection.off(Connection.Events.CONNECTED, onConnectedOrDisconnected);
+ this.connection.off(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
+ if (this.connected) {
+ deferred.resolve();
+ } else {
+ deferred.reject();
+ }
+ };
+ this.connection.on(Connection.Events.CONNECTED, onConnectedOrDisconnected);
+ this.connection.on(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
+ try {
+ // Reset the connection's state to defaults
+ this.connection.resetOptions();
+ // Only watch for errors here. Final resolution occurs above, once
+ // we've reached the CONNECTED state.
+ this.selectedRuntime.connect(this.connection)
+ .then(null, e => deferred.reject(e));
+ } catch (e) {
+ deferred.reject(e);
+ }
+ }, deferred.reject);
+
+ // Record connection result in telemetry
+ let logResult = result => {
+ this._telemetry.log("DEVTOOLS_WEBIDE_CONNECTION_RESULT", result);
+ if (runtime.type) {
+ this._telemetry.log("DEVTOOLS_WEBIDE_" + runtime.type +
+ "_CONNECTION_RESULT", result);
+ }
+ };
+ deferred.promise.then(() => logResult(true), () => logResult(false));
+
+ // If successful, record connection time in telemetry
+ deferred.promise.then(() => {
+ const timerId = "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS";
+ this._telemetry.startTimer(timerId);
+ this.connection.once(Connection.Events.STATUS_CHANGED, () => {
+ this._telemetry.stopTimer(timerId);
+ });
+ }).catch(() => {
+ // Empty rejection handler to silence uncaught rejection warnings
+ // |connectToRuntime| caller should listen for rejections.
+ // Bug 1121100 may find a better way to silence these.
+ });
+
+ return deferred.promise;
+ },
+
+ _recordRuntimeInfo: Task.async(function* () {
+ if (!this.connected) {
+ return;
+ }
+ let runtime = this.selectedRuntime;
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
+ runtime.type || "UNKNOWN", true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
+ runtime.id || "unknown", true);
+ if (!this.deviceFront) {
+ this.update("runtime-telemetry");
+ return;
+ }
+ let d = yield this.deviceFront.getDescription();
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
+ d.processor, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
+ d.os, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
+ d.platformversion, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
+ d.apptype, true);
+ this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
+ d.version, true);
+ this.update("runtime-telemetry");
+ }),
+
+ isMainProcessDebuggable: function () {
+ // Fx <39 exposes chrome tab actors on RootActor
+ // Fx >=39 exposes a dedicated actor via getProcess request
+ return this.connection.client &&
+ this.connection.client.mainRoot &&
+ this.connection.client.mainRoot.traits.allowChromeProcess ||
+ (this._listTabsResponse &&
+ this._listTabsResponse.consoleActor);
+ },
+
+ get deviceFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getDeviceFront(this.connection.client, this._listTabsResponse);
+ },
+
+ get preferenceFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getPreferenceFront(this.connection.client, this._listTabsResponse);
+ },
+
+ get settingsFront() {
+ if (!this._listTabsResponse) {
+ return null;
+ }
+ return getSettingsFront(this.connection.client, this._listTabsResponse);
+ },
+
+ disconnectRuntime: function () {
+ if (!this.connected) {
+ return promise.resolve();
+ }
+ let deferred = promise.defer();
+ this.connection.once(Connection.Events.DISCONNECTED, () => deferred.resolve());
+ this.connection.disconnect();
+ return deferred.promise;
+ },
+
+ launchRuntimeApp: function () {
+ if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
+ return promise.reject("attempting to launch a non-runtime app");
+ }
+ let app = this._getProjectFront(this.selectedProject);
+ return app.launch();
+ },
+
+ launchOrReloadRuntimeApp: function () {
+ if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
+ return promise.reject("attempting to launch / reload a non-runtime app");
+ }
+ let app = this._getProjectFront(this.selectedProject);
+ if (!app.running) {
+ return app.launch();
+ } else {
+ return app.reload();
+ }
+ },
+
+ runtimeCanHandleApps: function () {
+ return !!this._appsFront;
+ },
+
+ installAndRunProject: function () {
+ let project = this.selectedProject;
+
+ if (!project || (project.type != "packaged" && project.type != "hosted")) {
+ console.error("Can't install project. Unknown type of project.");
+ return promise.reject("Can't install");
+ }
+
+ if (!this._listTabsResponse) {
+ this.reportError("error_cantInstallNotFullyConnected");
+ return promise.reject("Can't install");
+ }
+
+ if (!this._appsFront) {
+ console.error("Runtime doesn't have a webappsActor");
+ return promise.reject("Can't install");
+ }
+
+ return Task.spawn(function* () {
+ let self = AppManager;
+
+ // Package and validate project
+ yield self.packageProject(project);
+ yield self.validateAndUpdateProject(project);
+
+ if (project.errorsCount > 0) {
+ self.reportError("error_cantInstallValidationErrors");
+ return;
+ }
+
+ let installPromise;
+
+ if (project.type != "packaged" && project.type != "hosted") {
+ return promise.reject("Don't know how to install project");
+ }
+
+ let response;
+ if (project.type == "packaged") {
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ console.log("Installing app from " + packageDir);
+
+ response = yield self._appsFront.installPackaged(packageDir,
+ project.packagedAppOrigin);
+
+ // If the packaged app specified a custom origin override,
+ // we need to update the local project origin
+ project.packagedAppOrigin = response.appId;
+ // And ensure the indexed db on disk is also updated
+ AppProjects.update(project);
+ }
+
+ if (project.type == "hosted") {
+ let manifestURLObject = Services.io.newURI(project.location, null, null);
+ let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
+ let appId = origin.host;
+ let metadata = {
+ origin: origin.spec,
+ manifestURL: project.location
+ };
+ response = yield self._appsFront.installHosted(appId,
+ metadata,
+ project.manifest);
+ }
+
+ // Addons don't have any document to load (yet?)
+ // So that there is no need to run them, installing is enough
+ if (project.manifest.manifest_version || project.manifest.role === "addon") {
+ return;
+ }
+
+ let {app} = response;
+ if (!app.running) {
+ let deferred = promise.defer();
+ self.on("app-manager-update", function onUpdate(event, what) {
+ if (what == "project-started") {
+ self.off("app-manager-update", onUpdate);
+ deferred.resolve();
+ }
+ });
+ yield app.launch();
+ yield deferred.promise;
+ } else {
+ yield app.reload();
+ }
+ });
+ },
+
+ stopRunningApp: function () {
+ let app = this._getProjectFront(this.selectedProject);
+ return app.close();
+ },
+
+ /* PROJECT VALIDATION */
+
+ validateAndUpdateProject: function (project) {
+ if (!project) {
+ return promise.reject();
+ }
+
+ return Task.spawn(function* () {
+
+ let packageDir = yield ProjectBuilding.getPackageDir(project);
+ let validation = new AppValidator({
+ type: project.type,
+ // Build process may place the manifest in a non-root directory
+ location: packageDir
+ });
+
+ yield validation.validate();
+
+ if (validation.manifest) {
+ let manifest = validation.manifest;
+ let iconPath;
+ if (manifest.icons) {
+ let size = Object.keys(manifest.icons).sort((a, b) => b - a)[0];
+ if (size) {
+ iconPath = manifest.icons[size];
+ }
+ }
+ if (!iconPath) {
+ project.icon = AppManager.DEFAULT_PROJECT_ICON;
+ } else {
+ if (project.type == "hosted") {
+ let manifestURL = Services.io.newURI(project.location, null, null);
+ let origin = Services.io.newURI(manifestURL.prePath, null, null);
+ project.icon = Services.io.newURI(iconPath, null, origin).spec;
+ } else if (project.type == "packaged") {
+ let projectFolder = FileUtils.File(packageDir);
+ let folderURI = Services.io.newFileURI(projectFolder).spec;
+ project.icon = folderURI + iconPath.replace(/^\/|\\/, "");
+ }
+ }
+ project.manifest = validation.manifest;
+
+ if ("name" in project.manifest) {
+ project.name = project.manifest.name;
+ } else {
+ project.name = AppManager.DEFAULT_PROJECT_NAME;
+ }
+ } else {
+ project.manifest = null;
+ project.icon = AppManager.DEFAULT_PROJECT_ICON;
+ project.name = AppManager.DEFAULT_PROJECT_NAME;
+ }
+
+ project.validationStatus = "valid";
+
+ if (validation.warnings.length > 0) {
+ project.warningsCount = validation.warnings.length;
+ project.warnings = validation.warnings;
+ project.validationStatus = "warning";
+ } else {
+ project.warnings = "";
+ project.warningsCount = 0;
+ }
+
+ if (validation.errors.length > 0) {
+ project.errorsCount = validation.errors.length;
+ project.errors = validation.errors;
+ project.validationStatus = "error";
+ } else {
+ project.errors = "";
+ project.errorsCount = 0;
+ }
+
+ if (project.warningsCount && project.errorsCount) {
+ project.validationStatus = "error warning";
+ }
+
+ if (project.type === "hosted" && project.location !== validation.manifestURL) {
+ yield AppProjects.updateLocation(project, validation.manifestURL);
+ } else if (AppProjects.get(project.location)) {
+ yield AppProjects.update(project);
+ }
+
+ if (AppManager.selectedProject === project) {
+ AppManager.update("project-validated");
+ }
+ });
+ },
+
+ /* RUNTIME LIST */
+
+ _clearRuntimeList: function () {
+ this.runtimeList = {
+ usb: [],
+ wifi: [],
+ simulator: [],
+ other: []
+ };
+ },
+
+ _rebuildRuntimeList: function () {
+ let runtimes = RuntimeScanners.listRuntimes();
+ this._clearRuntimeList();
+
+ // Reorganize runtimes by type
+ for (let runtime of runtimes) {
+ switch (runtime.type) {
+ case RuntimeTypes.USB:
+ this.runtimeList.usb.push(runtime);
+ break;
+ case RuntimeTypes.WIFI:
+ this.runtimeList.wifi.push(runtime);
+ break;
+ case RuntimeTypes.SIMULATOR:
+ this.runtimeList.simulator.push(runtime);
+ break;
+ default:
+ this.runtimeList.other.push(runtime);
+ }
+ }
+
+ this.update("runtime-details");
+ this.update("runtime-list");
+ },
+
+ /* MANIFEST UTILS */
+
+ writeManifest: function (project) {
+ if (project.type != "packaged") {
+ return promise.reject("Not a packaged app");
+ }
+
+ if (!project.manifest) {
+ project.manifest = {};
+ }
+
+ let folder = project.location;
+ let manifestPath = OS.Path.join(folder, "manifest.webapp");
+ let text = JSON.stringify(project.manifest, null, 2);
+ let encoder = new TextEncoder();
+ let array = encoder.encode(text);
+ return OS.File.writeAtomic(manifestPath, array, {tmpPath: manifestPath + ".tmp"});
+ },
+};
+
+EventEmitter.decorate(AppManager);
diff --git a/devtools/client/webide/modules/app-projects.js b/devtools/client/webide/modules/app-projects.js
new file mode 100644
index 0000000000..691d090642
--- /dev/null
+++ b/devtools/client/webide/modules/app-projects.js
@@ -0,0 +1,235 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cc, Ci, Cu, Cr} = require("chrome");
+const promise = require("promise");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+
+/**
+ * IndexedDB wrapper that just save project objects
+ *
+ * The only constraint is that project objects have to have
+ * a unique `location` object.
+ */
+
+const IDB = {
+ _db: null,
+ databaseName: "AppProjects",
+
+ open: function () {
+ let deferred = promise.defer();
+
+ let request = indexedDB.open(IDB.databaseName, 5);
+ request.onerror = function (event) {
+ deferred.reject("Unable to open AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onupgradeneeded = function (event) {
+ let db = event.target.result;
+ db.createObjectStore("projects", { keyPath: "location" });
+ };
+
+ request.onsuccess = function () {
+ let db = IDB._db = request.result;
+ let objectStore = db.transaction("projects").objectStore("projects");
+ let projects = [];
+ let toRemove = [];
+ objectStore.openCursor().onsuccess = function (event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (cursor.value.location) {
+
+ // We need to make sure this object has a `.location` property.
+ // The UI depends on this property.
+ // This should not be needed as we make sure to register valid
+ // projects, but in the past (before bug 924568), we might have
+ // registered invalid objects.
+
+
+ // We also want to make sure the location is valid.
+ // If the location doesn't exist, we remove the project.
+
+ try {
+ let file = FileUtils.File(cursor.value.location);
+ if (file.exists()) {
+ projects.push(cursor.value);
+ } else {
+ toRemove.push(cursor.value.location);
+ }
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH) {
+ // A URL
+ projects.push(cursor.value);
+ }
+ }
+ }
+ cursor.continue();
+ } else {
+ let removePromises = [];
+ for (let location of toRemove) {
+ removePromises.push(IDB.remove(location));
+ }
+ promise.all(removePromises).then(() => {
+ deferred.resolve(projects);
+ });
+ }
+ };
+ };
+
+ return deferred.promise;
+ },
+
+ add: function (project) {
+ let deferred = promise.defer();
+
+ if (!project.location) {
+ // We need to make sure this object has a `.location` property.
+ deferred.reject("Missing location property on project object.");
+ } else {
+ let transaction = IDB._db.transaction(["projects"], "readwrite");
+ let objectStore = transaction.objectStore("projects");
+ let request = objectStore.add(project);
+ request.onerror = function (event) {
+ deferred.reject("Unable to add project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onsuccess = function () {
+ deferred.resolve();
+ };
+ }
+
+ return deferred.promise;
+ },
+
+ update: function (project) {
+ let deferred = promise.defer();
+
+ var transaction = IDB._db.transaction(["projects"], "readwrite");
+ var objectStore = transaction.objectStore("projects");
+ var request = objectStore.put(project);
+ request.onerror = function (event) {
+ deferred.reject("Unable to update project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+ request.onsuccess = function () {
+ deferred.resolve();
+ };
+
+ return deferred.promise;
+ },
+
+ remove: function (location) {
+ let deferred = promise.defer();
+
+ let request = IDB._db.transaction(["projects"], "readwrite")
+ .objectStore("projects")
+ .delete(location);
+ request.onsuccess = function (event) {
+ deferred.resolve();
+ };
+ request.onerror = function () {
+ deferred.reject("Unable to delete project to the AppProjects indexedDB: " +
+ this.error.name + " - " + this.error.message);
+ };
+
+ return deferred.promise;
+ }
+};
+
+var loadDeferred = promise.defer();
+
+loadDeferred.resolve(IDB.open().then(function (projects) {
+ AppProjects.projects = projects;
+ AppProjects.emit("ready", projects);
+}));
+
+const AppProjects = {
+ load: function () {
+ return loadDeferred.promise;
+ },
+
+ addPackaged: function (folder) {
+ let file = FileUtils.File(folder.path);
+ if (!file.exists()) {
+ return promise.reject("path doesn't exist");
+ }
+ let existingProject = this.get(folder.path);
+ if (existingProject) {
+ return promise.reject("Already added");
+ }
+ let project = {
+ type: "packaged",
+ location: folder.path,
+ // We need a unique id, that is the app origin,
+ // in order to identify the app when being installed on the device.
+ // The packaged app local path is a valid id, but only on the client.
+ // This origin will be used to generate the true id of an app:
+ // its manifest URL.
+ // If the app ends up specifying an explicit origin in its manifest,
+ // we will override this random UUID on app install.
+ packagedAppOrigin: generateUUID().toString().slice(1, -1)
+ };
+ return IDB.add(project).then(() => {
+ this.projects.push(project);
+ return project;
+ });
+ },
+
+ addHosted: function (manifestURL) {
+ let existingProject = this.get(manifestURL);
+ if (existingProject) {
+ return promise.reject("Already added");
+ }
+ let project = {
+ type: "hosted",
+ location: manifestURL
+ };
+ return IDB.add(project).then(() => {
+ this.projects.push(project);
+ return project;
+ });
+ },
+
+ update: function (project) {
+ return IDB.update(project);
+ },
+
+ updateLocation: function (project, newLocation) {
+ return IDB.remove(project.location)
+ .then(() => {
+ project.location = newLocation;
+ return IDB.add(project);
+ });
+ },
+
+ remove: function (location) {
+ return IDB.remove(location).then(() => {
+ for (let i = 0; i < this.projects.length; i++) {
+ if (this.projects[i].location == location) {
+ this.projects.splice(i, 1);
+ return;
+ }
+ }
+ throw new Error("Unable to find project in AppProjects store");
+ });
+ },
+
+ get: function (location) {
+ for (let i = 0; i < this.projects.length; i++) {
+ if (this.projects[i].location == location) {
+ return this.projects[i];
+ }
+ }
+ return null;
+ },
+
+ projects: []
+};
+
+EventEmitter.decorate(AppProjects);
+
+exports.AppProjects = AppProjects;
diff --git a/devtools/client/webide/modules/app-validator.js b/devtools/client/webide/modules/app-validator.js
new file mode 100644
index 0000000000..7507201103
--- /dev/null
+++ b/devtools/client/webide/modules/app-validator.js
@@ -0,0 +1,292 @@
+/* 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 {Ci, Cu, CC} = require("chrome");
+const promise = require("promise");
+
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+var XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
+var strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");
+
+function AppValidator({ type, location }) {
+ this.type = type;
+ this.location = location;
+ this.errors = [];
+ this.warnings = [];
+}
+
+AppValidator.prototype.error = function (message) {
+ this.errors.push(message);
+};
+
+AppValidator.prototype.warning = function (message) {
+ this.warnings.push(message);
+};
+
+AppValidator.prototype._getPackagedManifestFile = function () {
+ let manifestFile = FileUtils.File(this.location);
+ if (!manifestFile.exists()) {
+ this.error(strings.GetStringFromName("validator.nonExistingFolder"));
+ return null;
+ }
+ if (!manifestFile.isDirectory()) {
+ this.error(strings.GetStringFromName("validator.expectProjectFolder"));
+ return null;
+ }
+
+ let appManifestFile = manifestFile.clone();
+ appManifestFile.append("manifest.webapp");
+
+ let jsonManifestFile = manifestFile.clone();
+ jsonManifestFile.append("manifest.json");
+
+ let hasAppManifest = appManifestFile.exists() && appManifestFile.isFile();
+ let hasJsonManifest = jsonManifestFile.exists() && jsonManifestFile.isFile();
+
+ if (!hasAppManifest && !hasJsonManifest) {
+ this.error(strings.GetStringFromName("validator.noManifestFile"));
+ return null;
+ }
+
+ return hasAppManifest ? appManifestFile : jsonManifestFile;
+};
+
+AppValidator.prototype._getPackagedManifestURL = function () {
+ let manifestFile = this._getPackagedManifestFile();
+ if (!manifestFile) {
+ return null;
+ }
+ return Services.io.newFileURI(manifestFile).spec;
+};
+
+AppValidator.checkManifest = function (manifestURL) {
+ let deferred = promise.defer();
+ let error;
+
+ let req = new XMLHttpRequest();
+ req.overrideMimeType("text/plain");
+
+ try {
+ req.open("GET", manifestURL, true);
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+ } catch (e) {
+ error = strings.formatStringFromName("validator.invalidManifestURL", [manifestURL], 1);
+ deferred.reject(error);
+ return deferred.promise;
+ }
+
+ req.onload = function () {
+ let manifest = null;
+ try {
+ manifest = JSON.parse(req.responseText);
+ } catch (e) {
+ error = strings.formatStringFromName("validator.invalidManifestJSON", [e, manifestURL], 2);
+ deferred.reject(error);
+ }
+
+ deferred.resolve({manifest, manifestURL});
+ };
+
+ req.onerror = function () {
+ error = strings.formatStringFromName("validator.noAccessManifestURL", [req.statusText, manifestURL], 2);
+ deferred.reject(error);
+ };
+
+ try {
+ req.send(null);
+ } catch (e) {
+ error = strings.formatStringFromName("validator.noAccessManifestURL", [e, manifestURL], 2);
+ deferred.reject(error);
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.findManifestAtOrigin = function (manifestURL) {
+ let fixedManifest = Services.io.newURI(manifestURL, null, null).prePath + "/manifest.webapp";
+ return AppValidator.checkManifest(fixedManifest);
+};
+
+AppValidator.findManifestPath = function (manifestURL) {
+ let deferred = promise.defer();
+
+ if (manifestURL.endsWith("manifest.webapp")) {
+ deferred.reject();
+ } else {
+ let fixedManifest = manifestURL + "/manifest.webapp";
+ deferred.resolve(AppValidator.checkManifest(fixedManifest));
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.checkAlternateManifest = function (manifestURL) {
+ return Task.spawn(function* () {
+ let result;
+ try {
+ result = yield AppValidator.findManifestPath(manifestURL);
+ } catch (e) {
+ result = yield AppValidator.findManifestAtOrigin(manifestURL);
+ }
+
+ return result;
+ });
+};
+
+AppValidator.prototype._fetchManifest = function (manifestURL) {
+ let deferred = promise.defer();
+ this.manifestURL = manifestURL;
+
+ AppValidator.checkManifest(manifestURL)
+ .then(({manifest, manifestURL}) => {
+ deferred.resolve(manifest);
+ }, error => {
+ AppValidator.checkAlternateManifest(manifestURL)
+ .then(({manifest, manifestURL}) => {
+ this.manifestURL = manifestURL;
+ deferred.resolve(manifest);
+ }, () => {
+ this.error(error);
+ deferred.resolve(null);
+ });
+ });
+
+ return deferred.promise;
+};
+
+AppValidator.prototype._getManifest = function () {
+ let manifestURL;
+ if (this.type == "packaged") {
+ manifestURL = this._getPackagedManifestURL();
+ if (!manifestURL)
+ return promise.resolve(null);
+ } else if (this.type == "hosted") {
+ manifestURL = this.location;
+ try {
+ Services.io.newURI(manifestURL, null, null);
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.invalidHostedManifestURL", [manifestURL, e.message], 2));
+ return promise.resolve(null);
+ }
+ } else {
+ this.error(strings.formatStringFromName("validator.invalidProjectType", [this.type], 1));
+ return promise.resolve(null);
+ }
+ return this._fetchManifest(manifestURL);
+};
+
+AppValidator.prototype.validateManifest = function (manifest) {
+ if (!manifest.name) {
+ this.error(strings.GetStringFromName("validator.missNameManifestProperty"));
+ }
+
+ if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
+ this.warning(strings.GetStringFromName("validator.missIconsManifestProperty"));
+ } else if (!manifest.icons["128"]) {
+ this.warning(strings.GetStringFromName("validator.missIconMarketplace2"));
+ }
+};
+
+AppValidator.prototype._getOriginURL = function () {
+ if (this.type == "packaged") {
+ let manifestURL = Services.io.newURI(this.manifestURL, null, null);
+ return Services.io.newURI(".", null, manifestURL).spec;
+ } else if (this.type == "hosted") {
+ return Services.io.newURI(this.location, null, null).prePath;
+ }
+};
+
+AppValidator.prototype.validateLaunchPath = function (manifest) {
+ let deferred = promise.defer();
+ // The launch_path field has to start with a `/`
+ if (manifest.launch_path && manifest.launch_path[0] !== "/") {
+ this.error(strings.formatStringFromName("validator.nonAbsoluteLaunchPath", [manifest.launch_path], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+ let origin = this._getOriginURL();
+ let path;
+ if (this.type == "packaged") {
+ path = "." + (manifest.launch_path || "/index.html");
+ } else if (this.type == "hosted") {
+ path = manifest.launch_path || "/";
+ }
+ let indexURL;
+ try {
+ indexURL = Services.io.newURI(path, null, Services.io.newURI(origin, null, null)).spec;
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [origin + path], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+
+ let req = new XMLHttpRequest();
+ req.overrideMimeType("text/plain");
+ try {
+ req.open("HEAD", indexURL, true);
+ req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ return deferred.promise;
+ }
+ req.onload = () => {
+ if (req.status >= 400)
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [indexURL, req.status], 2));
+ deferred.resolve();
+ };
+ req.onerror = () => {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ };
+
+ try {
+ req.send(null);
+ } catch (e) {
+ this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
+ deferred.resolve();
+ }
+
+ return deferred.promise;
+};
+
+AppValidator.prototype.validateType = function (manifest) {
+ let appType = manifest.type || "web";
+ if (["web", "privileged", "certified"].indexOf(appType) === -1) {
+ this.error(strings.formatStringFromName("validator.invalidAppType", [appType], 1));
+ } else if (this.type == "hosted" &&
+ ["certified", "privileged"].indexOf(appType) !== -1) {
+ this.error(strings.formatStringFromName("validator.invalidHostedPriviledges", [appType], 1));
+ }
+
+ // certified app are not fully supported on the simulator
+ if (appType === "certified") {
+ this.warning(strings.GetStringFromName("validator.noCertifiedSupport"));
+ }
+};
+
+AppValidator.prototype.validate = function () {
+ this.errors = [];
+ this.warnings = [];
+ return this._getManifest().
+ then((manifest) => {
+ if (manifest) {
+ this.manifest = manifest;
+
+ // Skip validations for add-ons
+ if (manifest.role === "addon" || manifest.manifest_version) {
+ return promise.resolve();
+ }
+
+ this.validateManifest(manifest);
+ this.validateType(manifest);
+ return this.validateLaunchPath(manifest);
+ }
+ });
+};
+
+exports.AppValidator = AppValidator;
diff --git a/devtools/client/webide/modules/build.js b/devtools/client/webide/modules/build.js
new file mode 100644
index 0000000000..34cbcc0b75
--- /dev/null
+++ b/devtools/client/webide/modules/build.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cu, Cc, Ci} = require("chrome");
+
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+const { TextDecoder, OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
+const Subprocess = require("sdk/system/child_process/subprocess");
+
+const ProjectBuilding = exports.ProjectBuilding = {
+ fetchPackageManifest: Task.async(function* (project) {
+ let manifestPath = OS.Path.join(project.location, "package.json");
+ let exists = yield OS.File.exists(manifestPath);
+ if (!exists) {
+ // No explicit manifest, try to generate one if possible
+ return this.generatePackageManifest(project);
+ }
+
+ let data = yield OS.File.read(manifestPath);
+ data = new TextDecoder().decode(data);
+ let manifest;
+ try {
+ manifest = JSON.parse(data);
+ } catch (e) {
+ throw new Error("Error while reading WebIDE manifest at: '" + manifestPath +
+ "', invalid JSON: " + e.message);
+ }
+ return manifest;
+ }),
+
+ /**
+ * For common frameworks in the community, attempt to detect the build
+ * settings if none are defined. This makes it much easier to get started
+ * with WebIDE. Later on, perhaps an add-on could define such things for
+ * different frameworks.
+ */
+ generatePackageManifest: Task.async(function* (project) {
+ // Cordova
+ let cordovaConfigPath = OS.Path.join(project.location, "config.xml");
+ let exists = yield OS.File.exists(cordovaConfigPath);
+ if (!exists) {
+ return;
+ }
+ let data = yield OS.File.read(cordovaConfigPath);
+ data = new TextDecoder().decode(data);
+ if (data.contains("cordova.apache.org")) {
+ return {
+ "webide": {
+ "prepackage": "cordova prepare",
+ "packageDir": "./platforms/firefoxos/www"
+ }
+ };
+ }
+ }),
+
+ hasPrepackage: Task.async(function* (project) {
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+ return manifest && manifest.webide && "prepackage" in manifest.webide;
+ }),
+
+ // If the app depends on some build step, run it before pushing the app
+ build: Task.async(function* ({ project, logger }) {
+ if (!(yield this.hasPrepackage(project))) {
+ return;
+ }
+
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+
+ logger("start");
+ try {
+ yield this._build(project, manifest, logger);
+ logger("succeed");
+ } catch (e) {
+ logger("failed", e);
+ }
+ }),
+
+ _build: Task.async(function* (project, manifest, logger) {
+ // Look for `webide` property
+ manifest = manifest.webide;
+
+ let command, cwd, args = [], env = [];
+
+ // Copy frequently used env vars
+ let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ ["HOME", "PATH"].forEach(key => {
+ let value = envService.get(key);
+ if (value) {
+ env.push(key + "=" + value);
+ }
+ });
+
+ if (typeof (manifest.prepackage) === "string") {
+ command = manifest.prepackage.replace(/%project%/g, project.location);
+ } else if (manifest.prepackage.command) {
+ command = manifest.prepackage.command;
+
+ args = manifest.prepackage.args || [];
+ args = args.map(a => a.replace(/%project%/g, project.location));
+
+ env = env.concat(manifest.prepackage.env || []);
+ env = env.map(a => a.replace(/%project%/g, project.location));
+
+ if (manifest.prepackage.cwd) {
+ // Normalize path for Windows support (converts / to \)
+ let path = OS.Path.normalize(manifest.prepackage.cwd);
+ // Note that Path.join also support absolute path and argument.
+ // So that if cwd is absolute, it will return cwd.
+ let rel = OS.Path.join(project.location, path);
+ let exists = yield OS.File.exists(rel);
+ if (exists) {
+ cwd = rel;
+ }
+ }
+ } else {
+ throw new Error("pre-package manifest is invalid, missing or invalid " +
+ "`prepackage` attribute");
+ }
+
+ if (!cwd) {
+ cwd = project.location;
+ }
+
+ logger("Running pre-package hook '" + command + "' " +
+ args.join(" ") +
+ " with ENV=[" + env.join(", ") + "]" +
+ " at " + cwd);
+
+ // Run the command through a shell command in order to support non absolute
+ // paths.
+ // On Windows `ComSpec` env variable is going to refer to cmd.exe,
+ // Otherwise, on Linux and Mac, SHELL env variable should refer to
+ // the user chosen shell program.
+ // (We do not check for OS, as on windows, with cygwin, ComSpec isn't set)
+ let shell = envService.get("ComSpec") || envService.get("SHELL");
+ args.unshift(command);
+
+ // For cmd.exe, we have to pass the `/C` option,
+ // but for unix shells we need -c.
+ // That to interpret next argument as a shell command.
+ if (envService.exists("ComSpec")) {
+ args.unshift("/C");
+ } else {
+ args.unshift("-c");
+ }
+
+ // Subprocess changes CWD, we have to save and restore it.
+ let originalCwd = yield OS.File.getCurrentDirectory();
+ try {
+ let defer = promise.defer();
+ Subprocess.call({
+ command: shell,
+ arguments: args,
+ environment: env,
+ workdir: cwd,
+
+ stdout: data =>
+ logger(data),
+ stderr: data =>
+ logger(data),
+
+ done: result => {
+ logger("Terminated with error code: " + result.exitCode);
+ if (result.exitCode == 0) {
+ defer.resolve();
+ } else {
+ defer.reject("pre-package command failed with error code " + result.exitCode);
+ }
+ }
+ });
+ defer.promise.then(() => {
+ OS.File.setCurrentDirectory(originalCwd);
+ });
+ yield defer.promise;
+ } catch (e) {
+ throw new Error("Unable to run pre-package command '" + command + "' " +
+ args.join(" ") + ":\n" + (e.message || e));
+ }
+ }),
+
+ getPackageDir: Task.async(function* (project) {
+ let manifest = yield ProjectBuilding.fetchPackageManifest(project);
+ if (!manifest || !manifest.webide || !manifest.webide.packageDir) {
+ return project.location;
+ }
+ manifest = manifest.webide;
+
+ let packageDir = OS.Path.join(project.location, manifest.packageDir);
+ // On Windows, replace / by \\
+ packageDir = OS.Path.normalize(packageDir);
+ let exists = yield OS.File.exists(packageDir);
+ if (exists) {
+ return packageDir;
+ }
+ throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
+ })
+};
diff --git a/devtools/client/webide/modules/config-view.js b/devtools/client/webide/modules/config-view.js
new file mode 100644
index 0000000000..5fb07e235e
--- /dev/null
+++ b/devtools/client/webide/modules/config-view.js
@@ -0,0 +1,373 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cu} = require("chrome");
+
+const EventEmitter = require("devtools/shared/event-emitter");
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var ConfigView;
+
+module.exports = ConfigView = function (window) {
+ EventEmitter.decorate(this);
+ this._doc = window.document;
+ this._keys = [];
+ return this;
+};
+
+ConfigView.prototype = {
+ _renderByType: function (input, name, value, customType) {
+ value = customType || typeof value;
+
+ switch (value) {
+ case "boolean":
+ input.setAttribute("data-type", "boolean");
+ input.setAttribute("type", "checkbox");
+ break;
+ case "number":
+ input.setAttribute("data-type", "number");
+ input.setAttribute("type", "number");
+ break;
+ case "object":
+ input.setAttribute("data-type", "object");
+ input.setAttribute("type", "text");
+ break;
+ default:
+ input.setAttribute("data-type", "string");
+ input.setAttribute("type", "text");
+ break;
+ }
+ return input;
+ },
+
+ set front(front) {
+ this._front = front;
+ },
+
+ set keys(keys) {
+ this._keys = keys;
+ },
+
+ get keys() {
+ return this._keys;
+ },
+
+ set kind(kind) {
+ this._kind = kind;
+ },
+
+ set includeTypeName(include) {
+ this._includeTypeName = include;
+ },
+
+ search: function (event) {
+ if (event.target.value.length) {
+ let stringMatch = new RegExp(event.target.value, "i");
+
+ for (let i = 0; i < this._keys.length; i++) {
+ let key = this._keys[i];
+ let row = this._doc.getElementById("row-" + key);
+ if (key.match(stringMatch)) {
+ row.classList.remove("hide");
+ } else if (row) {
+ row.classList.add("hide");
+ }
+ }
+ } else {
+ var trs = this._doc.getElementById("device-fields").querySelectorAll("tr");
+
+ for (let i = 0; i < trs.length; i++) {
+ trs[i].classList.remove("hide");
+ }
+ }
+ },
+
+ generateDisplay: function (json) {
+ let deviceItems = Object.keys(json);
+ deviceItems.sort();
+ this.keys = deviceItems;
+ for (let i = 0; i < this.keys.length; i++) {
+ let key = this.keys[i];
+ this.generateField(key, json[key].value, json[key].hasUserValue);
+ }
+ },
+
+ generateField: function (name, value, hasUserValue, customType, newRow) {
+ let table = this._doc.querySelector("table");
+ let sResetDefault = Strings.GetStringFromName("device_reset_default");
+
+ if (this._keys.indexOf(name) === -1) {
+ this._keys.push(name);
+ }
+
+ let input = this._doc.createElement("input");
+ let tr = this._doc.createElement("tr");
+ tr.setAttribute("id", "row-" + name);
+ tr.classList.add("edit-row");
+ let td = this._doc.createElement("td");
+ td.classList.add("field-name");
+ td.textContent = name;
+ tr.appendChild(td);
+ td = this._doc.createElement("td");
+ input.classList.add("editable");
+ input.setAttribute("id", name);
+ input = this._renderByType(input, name, value, customType);
+
+ if (customType === "boolean" || input.type === "checkbox") {
+ input.checked = value;
+ } else {
+ if (typeof value === "object") {
+ value = JSON.stringify(value);
+ }
+ input.value = value;
+ }
+
+ if (!(this._includeTypeName || isNaN(parseInt(value, 10)))) {
+ input.type = "number";
+ }
+
+ td.appendChild(input);
+ tr.appendChild(td);
+ td = this._doc.createElement("td");
+ td.setAttribute("id", "td-" + name);
+
+ let button = this._doc.createElement("button");
+ button.setAttribute("data-id", name);
+ button.setAttribute("id", "btn-" + name);
+ button.classList.add("reset");
+ button.textContent = sResetDefault;
+ td.appendChild(button);
+
+ if (!hasUserValue) {
+ button.classList.add("hide");
+ }
+
+ tr.appendChild(td);
+
+ // If this is a new field, add it to the top of the table.
+ if (newRow) {
+ let existing = table.querySelector("#" + name);
+
+ if (!existing) {
+ table.insertBefore(tr, newRow);
+ } else {
+ existing.value = value;
+ }
+ } else {
+ table.appendChild(tr);
+ }
+ },
+
+ resetTable: function () {
+ let table = this._doc.querySelector("table");
+ let trs = table.querySelectorAll("tr:not(#add-custom-field)");
+
+ for (var i = 0; i < trs.length; i++) {
+ table.removeChild(trs[i]);
+ }
+
+ return table;
+ },
+
+ _getCallType: function (type, name) {
+ let frontName = "get";
+
+ if (this._includeTypeName) {
+ frontName += type;
+ }
+
+ return this._front[frontName + this._kind](name);
+ },
+
+ _setCallType: function (type, name, value) {
+ let frontName = "set";
+
+ if (this._includeTypeName) {
+ frontName += type;
+ }
+
+ return this._front[frontName + this._kind](name, value);
+ },
+
+ _saveByType: function (options) {
+ let fieldName = options.id;
+ let inputType = options.type;
+ let value = options.value;
+ let input = this._doc.getElementById(fieldName);
+
+ switch (inputType) {
+ case "boolean":
+ this._setCallType("Bool", fieldName, input.checked);
+ break;
+ case "number":
+ this._setCallType("Int", fieldName, value);
+ break;
+ case "object":
+ try {
+ value = JSON.parse(value);
+ } catch (e) {}
+ this._setCallType("Object", fieldName, value);
+ break;
+ default:
+ this._setCallType("Char", fieldName, value);
+ break;
+ }
+ },
+
+ updateField: function (event) {
+ if (event.target) {
+ let inputType = event.target.getAttribute("data-type");
+ let inputValue = event.target.checked || event.target.value;
+
+ if (event.target.nodeName == "input" &&
+ event.target.validity.valid &&
+ event.target.classList.contains("editable")) {
+ let id = event.target.id;
+ if (inputType === "boolean") {
+ if (event.target.checked) {
+ inputValue = true;
+ } else {
+ inputValue = false;
+ }
+ }
+
+ this._saveByType({
+ id: id,
+ type: inputType,
+ value: inputValue
+ });
+ this._doc.getElementById("btn-" + id).classList.remove("hide");
+ }
+ }
+ },
+
+ _resetToDefault: function (name, input, button) {
+ this._front["clearUser" + this._kind](name);
+ let dataType = input.getAttribute("data-type");
+ let tr = this._doc.getElementById("row-" + name);
+
+ switch (dataType) {
+ case "boolean":
+ this._defaultField = this._getCallType("Bool", name);
+ this._defaultField.then(boolean => {
+ input.checked = boolean;
+ }, () => {
+ input.checked = false;
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ case "number":
+ this._defaultField = this._getCallType("Int", name);
+ this._defaultField.then(number => {
+ input.value = number;
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ case "object":
+ this._defaultField = this._getCallType("Object", name);
+ this._defaultField.then(object => {
+ input.value = JSON.stringify(object);
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ default:
+ this._defaultField = this._getCallType("Char", name);
+ this._defaultField.then(string => {
+ input.value = string;
+ }, () => {
+ tr.parentNode.removeChild(tr);
+ });
+ break;
+ }
+
+ button.classList.add("hide");
+ },
+
+ checkReset: function (event) {
+ if (event.target.classList.contains("reset")) {
+ let btnId = event.target.getAttribute("data-id");
+ let input = this._doc.getElementById(btnId);
+ this._resetToDefault(btnId, input, event.target);
+ }
+ },
+
+ updateFieldType: function () {
+ let table = this._doc.querySelector("table");
+ let customValueType = table.querySelector("#custom-value-type").value;
+ let customTextEl = table.querySelector("#custom-value-text");
+ let customText = customTextEl.value;
+
+ if (customValueType.length === 0) {
+ return false;
+ }
+
+ switch (customValueType) {
+ case "boolean":
+ customTextEl.type = "checkbox";
+ customText = customTextEl.checked;
+ break;
+ case "number":
+ customText = parseInt(customText, 10) || 0;
+ customTextEl.type = "number";
+ break;
+ default:
+ customTextEl.type = "text";
+ break;
+ }
+
+ return customValueType;
+ },
+
+ clearNewFields: function () {
+ let table = this._doc.querySelector("table");
+ let customTextEl = table.querySelector("#custom-value-text");
+ if (customTextEl.checked) {
+ customTextEl.checked = false;
+ } else {
+ customTextEl.value = "";
+ }
+
+ this.updateFieldType();
+ },
+
+ updateNewField: function () {
+ let table = this._doc.querySelector("table");
+ let customValueType = this.updateFieldType();
+
+ if (!customValueType) {
+ return;
+ }
+
+ let customRow = table.querySelector("tr:nth-of-type(2)");
+ let customTextEl = table.querySelector("#custom-value-text");
+ let customTextNameEl = table.querySelector("#custom-value-name");
+
+ if (customTextEl.validity.valid) {
+ let customText = customTextEl.value;
+
+ if (customValueType === "boolean") {
+ customText = customTextEl.checked;
+ }
+
+ let customTextName = customTextNameEl.value.replace(/[^A-Za-z0-9\.\-_]/gi, "");
+ this.generateField(customTextName, customText, true, customValueType, customRow);
+ this._saveByType({
+ id: customTextName,
+ type: customValueType,
+ value: customText
+ });
+ customTextNameEl.value = "";
+ this.clearNewFields();
+ }
+ },
+
+ checkNewFieldSubmit: function (event) {
+ if (event.keyCode === 13) {
+ this._doc.getElementById("custom-value").click();
+ }
+ }
+};
diff --git a/devtools/client/webide/modules/moz.build b/devtools/client/webide/modules/moz.build
new file mode 100644
index 0000000000..c4072b7033
--- /dev/null
+++ b/devtools/client/webide/modules/moz.build
@@ -0,0 +1,21 @@
+# -*- 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/.
+
+DevToolsModules(
+ 'addons.js',
+ 'app-manager.js',
+ 'app-projects.js',
+ 'app-validator.js',
+ 'build.js',
+ 'config-view.js',
+ 'project-list.js',
+ 'runtime-list.js',
+ 'runtimes.js',
+ 'simulator-process.js',
+ 'simulators.js',
+ 'tab-store.js',
+ 'utils.js'
+)
diff --git a/devtools/client/webide/modules/project-list.js b/devtools/client/webide/modules/project-list.js
new file mode 100644
index 0000000000..10766dd4f2
--- /dev/null
+++ b/devtools/client/webide/modules/project-list.js
@@ -0,0 +1,375 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+* License, v. 2.0. If a copy of the MPL was not distributed with this
+* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {Cu} = require("chrome");
+
+const Services = require("Services");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const promise = require("promise");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {Task} = require("devtools/shared/task");
+const utils = require("devtools/client/webide/modules/utils");
+const Telemetry = require("devtools/client/shared/telemetry");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var ProjectList;
+
+module.exports = ProjectList = function (win, parentWindow) {
+ EventEmitter.decorate(this);
+ this._doc = win.document;
+ this._UI = parentWindow.UI;
+ this._parentWindow = parentWindow;
+ this._telemetry = new Telemetry();
+ this._panelNodeEl = "div";
+
+ this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
+ this._UI.on("webide-update", this.onWebIDEUpdate);
+
+ AppManager.init();
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+};
+
+ProjectList.prototype = {
+ get doc() {
+ return this._doc;
+ },
+
+ 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 "project-removed":
+ case "runtime-apps-icons":
+ case "runtime-targets":
+ case "connection":
+ this.update(details);
+ break;
+ case "project":
+ this.updateCommands();
+ this.update(details);
+ break;
+ }
+ },
+
+ onWebIDEUpdate: function (event, what, details) {
+ if (what == "busy" || what == "unbusy") {
+ this.updateCommands();
+ }
+ },
+
+ /**
+ * testOptions: { chrome mochitest support
+ * folder: nsIFile, where to store the app
+ * index: Number, index of the app in the template list
+ * name: String name of the app
+ * }
+ */
+ newApp: function (testOptions) {
+ let parentWindow = this._parentWindow;
+ let self = this;
+ return this._UI.busyUntil(Task.spawn(function* () {
+ // Open newapp.xul, which will feed ret.location
+ let ret = {location: null, testOptions: testOptions};
+ parentWindow.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret);
+ if (!ret.location)
+ return;
+
+ // Retrieve added project
+ let project = AppProjects.get(ret.location);
+
+ // Select project
+ AppManager.selectedProject = project;
+
+ self._telemetry.actionOccurred("webideNewProject");
+ }), "creating new app");
+ },
+
+ importPackagedApp: function (location) {
+ let parentWindow = this._parentWindow;
+ let UI = this._UI;
+ return UI.busyUntil(Task.spawn(function* () {
+ let directory = utils.getPackagedDirectory(parentWindow, location);
+
+ if (!directory) {
+ // User cancelled directory selection
+ return;
+ }
+
+ yield UI.importAndSelectApp(directory);
+ }), "importing packaged app");
+ },
+
+ importHostedApp: function (location) {
+ let parentWindow = this._parentWindow;
+ let UI = this._UI;
+ return UI.busyUntil(Task.spawn(function* () {
+ let url = utils.getHostedURL(parentWindow, location);
+
+ if (!url) {
+ return;
+ }
+
+ yield UI.importAndSelectApp(url);
+ }), "importing hosted app");
+ },
+
+ /**
+ * opts: {
+ * panel: Object, currenl project panel node
+ * name: String, name of the project
+ * icon: String path of the project icon
+ * }
+ */
+ _renderProjectItem: function (opts) {
+ let span = opts.panel.querySelector("span") || this._doc.createElement("span");
+ span.textContent = opts.name;
+ let icon = opts.panel.querySelector("img") || this._doc.createElement("img");
+ icon.className = "project-image";
+ icon.setAttribute("src", opts.icon);
+ opts.panel.appendChild(icon);
+ opts.panel.appendChild(span);
+ opts.panel.setAttribute("title", opts.name);
+ },
+
+ refreshTabs: function () {
+ if (AppManager.connected) {
+ return AppManager.listTabs().then(() => {
+ this.updateTabs();
+ }).catch(console.error);
+ }
+ },
+
+ updateTabs: function () {
+ let tabsHeaderNode = this._doc.querySelector("#panel-header-tabs");
+ let tabsNode = this._doc.querySelector("#project-panel-tabs");
+
+ while (tabsNode.hasChildNodes()) {
+ tabsNode.firstChild.remove();
+ }
+
+ if (!AppManager.connected) {
+ tabsHeaderNode.setAttribute("hidden", "true");
+ return;
+ }
+
+ let tabs = AppManager.tabStore.tabs;
+
+ tabsHeaderNode.removeAttribute("hidden");
+
+ for (let i = 0; i < tabs.length; i++) {
+ let tab = tabs[i];
+ let URL = this._parentWindow.URL;
+ let url;
+ try {
+ url = new URL(tab.url);
+ } catch (e) {
+ // Don't try to handle invalid URLs, especially from Valence.
+ continue;
+ }
+ // Wanted to use nsIFaviconService here, but it only works for visited
+ // tabs, so that's no help for any remote tabs. Maybe some favicon wizard
+ // knows how to get high-res favicons easily, or we could offer actor
+ // support for this (bug 1061654).
+ if (url.origin) {
+ tab.favicon = url.origin + "/favicon.ico";
+ }
+ tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
+ if (url.protocol.startsWith("http")) {
+ tab.name = url.hostname + ": " + tab.name;
+ }
+ let panelItemNode = this._doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ tabsNode.appendChild(panelItemNode);
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: tab.name,
+ icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON
+ });
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "tab",
+ app: tab,
+ icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON,
+ location: tab.url,
+ name: tab.name
+ };
+ }, true);
+ }
+
+ return promise.resolve();
+ },
+
+ updateApps: function () {
+ let doc = this._doc;
+ let runtimeappsHeaderNode = doc.querySelector("#panel-header-runtimeapps");
+ let sortedApps = [];
+ for (let [manifestURL, app] of AppManager.apps) {
+ sortedApps.push(app);
+ }
+ sortedApps = sortedApps.sort((a, b) => {
+ return a.manifest.name > b.manifest.name;
+ });
+ let mainProcess = AppManager.isMainProcessDebuggable();
+ if (AppManager.connected && (sortedApps.length > 0 || mainProcess)) {
+ runtimeappsHeaderNode.removeAttribute("hidden");
+ } else {
+ runtimeappsHeaderNode.setAttribute("hidden", "true");
+ }
+
+ let runtimeAppsNode = doc.querySelector("#project-panel-runtimeapps");
+ while (runtimeAppsNode.hasChildNodes()) {
+ runtimeAppsNode.firstChild.remove();
+ }
+
+ if (mainProcess) {
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ });
+ runtimeAppsNode.appendChild(panelItemNode);
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "mainProcess",
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ };
+ }, true);
+ }
+
+ for (let i = 0; i < sortedApps.length; i++) {
+ let app = sortedApps[i];
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: app.manifest.name,
+ icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON
+ });
+ runtimeAppsNode.appendChild(panelItemNode);
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = {
+ type: "runtimeApp",
+ app: app.manifest,
+ icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON,
+ name: app.manifest.name
+ };
+ }, true);
+ }
+
+ return promise.resolve();
+ },
+
+ updateCommands: function () {
+ let doc = this._doc;
+ let newAppCmd;
+ let packagedAppCmd;
+ let hostedAppCmd;
+
+ newAppCmd = doc.querySelector("#new-app");
+ packagedAppCmd = doc.querySelector("#packaged-app");
+ hostedAppCmd = doc.querySelector("#hosted-app");
+
+ if (!newAppCmd || !packagedAppCmd || !hostedAppCmd) {
+ return;
+ }
+
+ if (this._parentWindow.document.querySelector("window").classList.contains("busy")) {
+ newAppCmd.setAttribute("disabled", "true");
+ packagedAppCmd.setAttribute("disabled", "true");
+ hostedAppCmd.setAttribute("disabled", "true");
+ return;
+ }
+
+ newAppCmd.removeAttribute("disabled");
+ packagedAppCmd.removeAttribute("disabled");
+ hostedAppCmd.removeAttribute("disabled");
+ },
+
+ /**
+ * Trigger an update of the project and remote runtime list.
+ * @param options object (optional)
+ * An |options| object containing a type of |apps| or |tabs| will limit
+ * what is updated to only those sections.
+ */
+ update: function (options) {
+ let deferred = promise.defer();
+
+ if (options && options.type === "apps") {
+ return this.updateApps();
+ } else if (options && options.type === "tabs") {
+ return this.updateTabs();
+ }
+
+ let doc = this._doc;
+ let projectsNode = doc.querySelector("#project-panel-projects");
+
+ while (projectsNode.hasChildNodes()) {
+ projectsNode.firstChild.remove();
+ }
+
+ AppProjects.load().then(() => {
+ let projects = AppProjects.projects;
+ for (let i = 0; i < projects.length; i++) {
+ let project = projects[i];
+ let panelItemNode = doc.createElement(this._panelNodeEl);
+ panelItemNode.className = "panel-item";
+ projectsNode.appendChild(panelItemNode);
+ if (!project.validationStatus) {
+ // The result of the validation process (storing names, icons, …) is not stored in
+ // the IndexedDB database when App Manager v1 is used.
+ // We need to run the validation again and update the name and icon of the app.
+ AppManager.validateAndUpdateProject(project).then(() => {
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: project.name,
+ icon: project.icon
+ });
+ });
+ } else {
+ this._renderProjectItem({
+ panel: panelItemNode,
+ name: project.name || AppManager.DEFAULT_PROJECT_NAME,
+ icon: project.icon || AppManager.DEFAULT_PROJECT_ICON
+ });
+ }
+ panelItemNode.addEventListener("click", () => {
+ AppManager.selectedProject = project;
+ }, true);
+ }
+
+ deferred.resolve();
+ }, deferred.reject);
+
+ // List remote apps and the main process, if they exist
+ this.updateApps();
+
+ // Build the tab list right now, so it's fast...
+ this.updateTabs();
+
+ // But re-list them and rebuild, in case any tabs navigated since the last
+ // time they were listed.
+ if (AppManager.connected) {
+ AppManager.listTabs().then(() => {
+ this.updateTabs();
+ }).catch(console.error);
+ }
+
+ return deferred.promise;
+ },
+
+ destroy: function () {
+ this._doc = null;
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ this._UI.off("webide-update", this.onWebIDEUpdate);
+ this._UI = null;
+ this._parentWindow = null;
+ this._panelNodeEl = null;
+ }
+};
diff --git a/devtools/client/webide/modules/runtime-list.js b/devtools/client/webide/modules/runtime-list.js
new file mode 100644
index 0000000000..295dd17051
--- /dev/null
+++ b/devtools/client/webide/modules/runtime-list.js
@@ -0,0 +1,207 @@
+/* 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 Services = require("Services");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+const {RuntimeScanners, WiFiScanner} = require("devtools/client/webide/modules/runtimes");
+const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Task} = require("devtools/shared/task");
+const utils = require("devtools/client/webide/modules/utils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+var RuntimeList;
+
+module.exports = RuntimeList = function (window, parentWindow) {
+ EventEmitter.decorate(this);
+ this._doc = window.document;
+ this._UI = parentWindow.UI;
+ this._Cmds = parentWindow.Cmds;
+ this._parentWindow = parentWindow;
+ this._panelNodeEl = "button";
+ this._panelBoxEl = "div";
+
+ this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
+ this._UI.on("webide-update", this.onWebIDEUpdate);
+
+ AppManager.init();
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+};
+
+RuntimeList.prototype = {
+ get doc() {
+ return this._doc;
+ },
+
+ 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.update();
+ break;
+ case "connection":
+ case "runtime-global-actors":
+ this.updateCommands();
+ break;
+ }
+ },
+
+ onWebIDEUpdate: function (event, what, details) {
+ if (what == "busy" || what == "unbusy") {
+ this.updateCommands();
+ }
+ },
+
+ takeScreenshot: function () {
+ this._Cmds.takeScreenshot();
+ },
+
+ showRuntimeDetails: function () {
+ this._Cmds.showRuntimeDetails();
+ },
+
+ showPermissionsTable: function () {
+ this._Cmds.showPermissionsTable();
+ },
+
+ showDevicePreferences: function () {
+ this._Cmds.showDevicePrefs();
+ },
+
+ showSettings: function () {
+ this._Cmds.showSettings();
+ },
+
+ showTroubleShooting: function () {
+ this._Cmds.showTroubleShooting();
+ },
+
+ showAddons: function () {
+ this._Cmds.showAddons();
+ },
+
+ refreshScanners: function () {
+ RuntimeScanners.scan();
+ },
+
+ updateCommands: function () {
+ let doc = this._doc;
+
+ // Runtime commands
+ let screenshotCmd = doc.querySelector("#runtime-screenshot");
+ let permissionsCmd = doc.querySelector("#runtime-permissions");
+ let detailsCmd = doc.querySelector("#runtime-details");
+ let disconnectCmd = doc.querySelector("#runtime-disconnect");
+ let devicePrefsCmd = doc.querySelector("#runtime-preferences");
+ let settingsCmd = doc.querySelector("#runtime-settings");
+
+ if (AppManager.connected) {
+ if (AppManager.deviceFront) {
+ 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 {
+ detailsCmd.setAttribute("disabled", "true");
+ permissionsCmd.setAttribute("disabled", "true");
+ screenshotCmd.setAttribute("disabled", "true");
+ disconnectCmd.setAttribute("disabled", "true");
+ devicePrefsCmd.setAttribute("disabled", "true");
+ settingsCmd.setAttribute("disabled", "true");
+ }
+ },
+
+ update: function () {
+ let doc = this._doc;
+ let wifiHeaderNode = doc.querySelector("#runtime-header-wifi");
+
+ if (WiFiScanner.allowed) {
+ wifiHeaderNode.removeAttribute("hidden");
+ } else {
+ wifiHeaderNode.setAttribute("hidden", "true");
+ }
+
+ let usbListNode = doc.querySelector("#runtime-panel-usb");
+ let wifiListNode = doc.querySelector("#runtime-panel-wifi");
+ let simulatorListNode = doc.querySelector("#runtime-panel-simulator");
+ let otherListNode = doc.querySelector("#runtime-panel-other");
+ let noHelperNode = doc.querySelector("#runtime-panel-noadbhelper");
+ let noUSBNode = doc.querySelector("#runtime-panel-nousbdevice");
+
+ if (Devices.helperAddonInstalled) {
+ noHelperNode.setAttribute("hidden", "true");
+ } else {
+ noHelperNode.removeAttribute("hidden");
+ }
+
+ let runtimeList = AppManager.runtimeList;
+
+ if (!runtimeList) {
+ return;
+ }
+
+ if (runtimeList.usb.length === 0 && Devices.helperAddonInstalled) {
+ noUSBNode.removeAttribute("hidden");
+ } else {
+ noUSBNode.setAttribute("hidden", "true");
+ }
+
+ for (let [type, parent] of [
+ ["usb", usbListNode],
+ ["wifi", wifiListNode],
+ ["simulator", simulatorListNode],
+ ["other", otherListNode],
+ ]) {
+ while (parent.hasChildNodes()) {
+ parent.firstChild.remove();
+ }
+ for (let runtime of runtimeList[type]) {
+ let r = runtime;
+ let panelItemNode = doc.createElement(this._panelBoxEl);
+ panelItemNode.className = "panel-item-complex";
+
+ let connectButton = doc.createElement(this._panelNodeEl);
+ connectButton.className = "panel-item runtime-panel-item-" + type;
+ connectButton.textContent = r.name;
+
+ connectButton.addEventListener("click", () => {
+ this._UI.dismissErrorNotification();
+ this._UI.connectToRuntime(r);
+ }, true);
+ panelItemNode.appendChild(connectButton);
+
+ if (r.configure) {
+ let configButton = doc.createElement(this._panelNodeEl);
+ configButton.className = "configure-button";
+ configButton.addEventListener("click", r.configure.bind(r), true);
+ panelItemNode.appendChild(configButton);
+ }
+
+ parent.appendChild(panelItemNode);
+ }
+ }
+ },
+
+ destroy: function () {
+ this._doc = null;
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ this._UI.off("webide-update", this.onWebIDEUpdate);
+ this._UI = null;
+ this._Cmds = null;
+ this._parentWindow = null;
+ this._panelNodeEl = null;
+ }
+};
diff --git a/devtools/client/webide/modules/runtimes.js b/devtools/client/webide/modules/runtimes.js
new file mode 100644
index 0000000000..a233373590
--- /dev/null
+++ b/devtools/client/webide/modules/runtimes.js
@@ -0,0 +1,673 @@
+/* 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 {Ci} = require("chrome");
+const Services = require("Services");
+const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {DebuggerServer} = require("devtools/server/main");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const discovery = require("devtools/shared/discovery/discovery");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+loader.lazyRequireGetter(this, "AuthenticationResult",
+ "devtools/shared/security/auth", true);
+loader.lazyRequireGetter(this, "DevToolsUtils",
+ "devtools/shared/DevToolsUtils");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+/**
+ * Runtime and Scanner API
+ *
+ * |RuntimeScanners| maintains a set of |Scanner| objects that produce one or
+ * more |Runtime|s to connect to. Add-ons can extend the set of known runtimes
+ * by registering additional |Scanner|s that emit them.
+ *
+ * Each |Scanner| must support the following API:
+ *
+ * enable()
+ * Bind any event handlers and start any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the first consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list enables the registry.
+ * disable()
+ * Unbind any event handlers and stop any background work the scanner needs to
+ * maintain an updated set of |Runtime|s.
+ * Called when the last consumer (such as WebIDE) actively interested in
+ * maintaining the |Runtime| list disables the registry.
+ * emits "runtime-list-updated"
+ * If the set of runtimes a |Scanner| manages has changed, it must emit this
+ * event to notify consumers of changes.
+ * scan()
+ * Actively refreshes the list of runtimes the scanner knows about. If your
+ * scanner uses an active scanning approach (as opposed to listening for
+ * events when changes occur), the bulk of the work would be done here.
+ * @return Promise
+ * Should be resolved when scanning is complete. If scanning has no
+ * well-defined end point, you can resolve immediately, as long as
+ * update event is emitted later when changes are noticed.
+ * listRuntimes()
+ * Return the current list of runtimes known to the |Scanner| instance.
+ * @return Iterable
+ *
+ * Each |Runtime| must support the following API:
+ *
+ * |type| field
+ * The |type| must be one of the values from the |RuntimeTypes| object. This
+ * is used for Telemetry and to support displaying sets of |Runtime|s
+ * categorized by type.
+ * |id| field
+ * An identifier that is unique in the set of all runtimes with the same
+ * |type|. WebIDE tries to save the last used runtime via type + id, and
+ * tries to locate it again in the next session, so this value should attempt
+ * to be stable across Firefox sessions.
+ * |name| field
+ * A user-visible label to identify the runtime that will be displayed in a
+ * runtime list.
+ * |prolongedConnection| field
+ * A boolean value which should be |true| if the connection process is
+ * expected to take a unknown or large amount of time. A UI may use this as a
+ * hint to skip timeouts or other time-based code paths.
+ * connect()
+ * Configure the passed |connection| object with any settings need to
+ * successfully connect to the runtime, and call the |connection|'s connect()
+ * method.
+ * @param Connection connection
+ * A |Connection| object from the DevTools |ConnectionManager|.
+ * @return Promise
+ * Resolved once you've called the |connection|'s connect() method.
+ * configure() OPTIONAL
+ * Show a configuration screen if the runtime is configurable.
+ */
+
+/* SCANNER REGISTRY */
+
+var RuntimeScanners = {
+
+ _enabledCount: 0,
+ _scanners: new Set(),
+
+ get enabled() {
+ return !!this._enabledCount;
+ },
+
+ add(scanner) {
+ if (this.enabled) {
+ // Enable any scanner added while globally enabled
+ this._enableScanner(scanner);
+ }
+ this._scanners.add(scanner);
+ this._emitUpdated();
+ },
+
+ remove(scanner) {
+ this._scanners.delete(scanner);
+ if (this.enabled) {
+ // Disable any scanner removed while globally enabled
+ this._disableScanner(scanner);
+ }
+ this._emitUpdated();
+ },
+
+ has(scanner) {
+ return this._scanners.has(scanner);
+ },
+
+ scan() {
+ if (!this.enabled) {
+ return promise.resolve();
+ }
+
+ if (this._scanPromise) {
+ return this._scanPromise;
+ }
+
+ let promises = [];
+
+ for (let scanner of this._scanners) {
+ promises.push(scanner.scan());
+ }
+
+ this._scanPromise = promise.all(promises);
+
+ // Reset pending promise
+ this._scanPromise.then(() => {
+ this._scanPromise = null;
+ }, () => {
+ this._scanPromise = null;
+ });
+
+ return this._scanPromise;
+ },
+
+ listRuntimes: function* () {
+ for (let scanner of this._scanners) {
+ for (let runtime of scanner.listRuntimes()) {
+ yield runtime;
+ }
+ }
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ enable() {
+ if (this._enabledCount++ !== 0) {
+ // Already enabled scanners during a previous call
+ return;
+ }
+ this._emitUpdated = this._emitUpdated.bind(this);
+ for (let scanner of this._scanners) {
+ this._enableScanner(scanner);
+ }
+ },
+
+ _enableScanner(scanner) {
+ scanner.enable();
+ scanner.on("runtime-list-updated", this._emitUpdated);
+ },
+
+ disable() {
+ if (--this._enabledCount !== 0) {
+ // Already disabled scanners during a previous call
+ return;
+ }
+ for (let scanner of this._scanners) {
+ this._disableScanner(scanner);
+ }
+ },
+
+ _disableScanner(scanner) {
+ scanner.off("runtime-list-updated", this._emitUpdated);
+ scanner.disable();
+ },
+
+};
+
+EventEmitter.decorate(RuntimeScanners);
+
+exports.RuntimeScanners = RuntimeScanners;
+
+/* SCANNERS */
+
+var SimulatorScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Simulators.on("updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Simulators.off("updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ Simulators.findSimulators().then(simulators => {
+ this._runtimes = [];
+ for (let simulator of simulators) {
+ this._runtimes.push(new SimulatorRuntime(simulator));
+ }
+ this._emitUpdated();
+ });
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(SimulatorScanner);
+RuntimeScanners.add(SimulatorScanner);
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This scanner will list all ADB devices as runtimes, even if they may or may
+ * not actually connect (since the |DeprecatedUSBRuntime| assumes a Firefox OS
+ * device).
+ */
+var DeprecatedAdbScanner = {
+
+ _runtimes: [],
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ Devices.on("register", this._updateRuntimes);
+ Devices.on("unregister", this._updateRuntimes);
+ Devices.on("addon-status-updated", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ Devices.off("register", this._updateRuntimes);
+ Devices.off("unregister", this._updateRuntimes);
+ Devices.off("addon-status-updated", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let id of Devices.available()) {
+ let runtime = new DeprecatedUSBRuntime(id);
+ this._runtimes.push(runtime);
+ runtime.updateNameFromADB().then(() => {
+ this._emitUpdated();
+ }, () => {});
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ }
+
+};
+
+EventEmitter.decorate(DeprecatedAdbScanner);
+RuntimeScanners.add(DeprecatedAdbScanner);
+
+// ADB Helper 0.7.0 and later will replace this scanner on startup
+exports.DeprecatedAdbScanner = DeprecatedAdbScanner;
+
+/**
+ * This is a lazy ADB scanner shim which only tells the ADB Helper to start and
+ * stop as needed. The real scanner that lists devices lives in ADB Helper.
+ * ADB Helper 0.8.0 and later wait until these signals are received before
+ * starting ADB polling. For earlier versions, they have no effect.
+ */
+var LazyAdbScanner = {
+
+ enable() {
+ Devices.emit("adb-start-polling");
+ },
+
+ disable() {
+ Devices.emit("adb-stop-polling");
+ },
+
+ scan() {
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return [];
+ }
+
+};
+
+EventEmitter.decorate(LazyAdbScanner);
+RuntimeScanners.add(LazyAdbScanner);
+
+var WiFiScanner = {
+
+ _runtimes: [],
+
+ init() {
+ this.updateRegistration();
+ Services.prefs.addObserver(this.ALLOWED_PREF, this, false);
+ },
+
+ enable() {
+ this._updateRuntimes = this._updateRuntimes.bind(this);
+ discovery.on("devtools-device-added", this._updateRuntimes);
+ discovery.on("devtools-device-updated", this._updateRuntimes);
+ discovery.on("devtools-device-removed", this._updateRuntimes);
+ this._updateRuntimes();
+ },
+
+ disable() {
+ discovery.off("devtools-device-added", this._updateRuntimes);
+ discovery.off("devtools-device-updated", this._updateRuntimes);
+ discovery.off("devtools-device-removed", this._updateRuntimes);
+ },
+
+ _emitUpdated() {
+ this.emit("runtime-list-updated");
+ },
+
+ _updateRuntimes() {
+ this._runtimes = [];
+ for (let device of discovery.getRemoteDevicesWithService("devtools")) {
+ this._runtimes.push(new WiFiRuntime(device));
+ }
+ this._emitUpdated();
+ },
+
+ scan() {
+ discovery.scan();
+ return promise.resolve();
+ },
+
+ listRuntimes: function () {
+ return this._runtimes;
+ },
+
+ ALLOWED_PREF: "devtools.remote.wifi.scan",
+
+ get allowed() {
+ return Services.prefs.getBoolPref(this.ALLOWED_PREF);
+ },
+
+ updateRegistration() {
+ if (this.allowed) {
+ RuntimeScanners.add(WiFiScanner);
+ } else {
+ RuntimeScanners.remove(WiFiScanner);
+ }
+ this._emitUpdated();
+ },
+
+ observe(subject, topic, data) {
+ if (data !== WiFiScanner.ALLOWED_PREF) {
+ return;
+ }
+ WiFiScanner.updateRegistration();
+ }
+
+};
+
+EventEmitter.decorate(WiFiScanner);
+WiFiScanner.init();
+
+exports.WiFiScanner = WiFiScanner;
+
+var StaticScanner = {
+ enable() {},
+ disable() {},
+ scan() { return promise.resolve(); },
+ listRuntimes() {
+ let runtimes = [gRemoteRuntime];
+ if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
+ runtimes.push(gLocalRuntime);
+ }
+ return runtimes;
+ }
+};
+
+EventEmitter.decorate(StaticScanner);
+RuntimeScanners.add(StaticScanner);
+
+/* RUNTIMES */
+
+// These type strings are used for logging events to Telemetry.
+// You must update Histograms.json if new types are added.
+var RuntimeTypes = exports.RuntimeTypes = {
+ USB: "USB",
+ WIFI: "WIFI",
+ SIMULATOR: "SIMULATOR",
+ REMOTE: "REMOTE",
+ LOCAL: "LOCAL",
+ OTHER: "OTHER"
+};
+
+/**
+ * TODO: Remove this comaptibility layer in the future (bug 1085393)
+ * This runtime exists to support the ADB Helper add-on below version 0.7.0.
+ *
+ * This runtime assumes it is connecting to a Firefox OS device.
+ */
+function DeprecatedUSBRuntime(id) {
+ this._id = id;
+}
+
+DeprecatedUSBRuntime.prototype = {
+ type: RuntimeTypes.USB,
+ get device() {
+ return Devices.getByName(this._id);
+ },
+ connect: function (connection) {
+ if (!this.device) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ return this.device.connect().then((port) => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.connect();
+ });
+ },
+ get id() {
+ return this._id;
+ },
+ get name() {
+ return this._productModel || this._id;
+ },
+ updateNameFromADB: function () {
+ if (this._productModel) {
+ return promise.reject();
+ }
+ let deferred = promise.defer();
+ if (this.device && this.device.shell) {
+ this.device.shell("getprop ro.product.model").then(stdout => {
+ this._productModel = stdout;
+ deferred.resolve();
+ }, () => {});
+ } else {
+ this._productModel = null;
+ deferred.reject();
+ }
+ return deferred.promise;
+ },
+};
+
+// For testing use only
+exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
+
+function WiFiRuntime(deviceName) {
+ this.deviceName = deviceName;
+}
+
+WiFiRuntime.prototype = {
+ type: RuntimeTypes.WIFI,
+ // Mark runtime as taking a long time to connect
+ prolongedConnection: true,
+ connect: function (connection) {
+ let service = discovery.getRemoteService("devtools", this.deviceName);
+ if (!service) {
+ return promise.reject(new Error("Can't find device: " + this.name));
+ }
+ connection.advertisement = service;
+ connection.authenticator.sendOOB = this.sendOOB;
+ // Disable the default connection timeout, since QR scanning can take an
+ // unknown amount of time. This prevents spurious errors (even after
+ // eventual success) from being shown.
+ connection.timeoutDelay = 0;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return this.deviceName;
+ },
+ get name() {
+ return this.deviceName;
+ },
+
+ /**
+ * During OOB_CERT authentication, a notification dialog like this is used to
+ * to display a token which the user must transfer through some mechanism to the
+ * server to authenticate the devices.
+ *
+ * This implementation presents the token as text for the user to transfer
+ * manually. For a mobile device, you should override this implementation with
+ * something more convenient, such as displaying a QR code.
+ *
+ * This method receives an object containing:
+ * @param host string
+ * The host name or IP address of the debugger server.
+ * @param port number
+ * The port number of the debugger server.
+ * @param cert object (optional)
+ * The server's cert details.
+ * @param authResult AuthenticationResult
+ * Authentication result sent from the server.
+ * @param oob object (optional)
+ * The token data to be transferred during OOB_CERT step 8:
+ * * sha256: hash(ClientCert)
+ * * k : K(random 128-bit number)
+ * @return object containing:
+ * * close: Function to hide the notification
+ */
+ sendOOB(session) {
+ const WINDOW_ID = "devtools:wifi-auth";
+ let { authResult } = session;
+ // Only show in the PENDING state
+ if (authResult != AuthenticationResult.PENDING) {
+ throw new Error("Expected PENDING result, got " + authResult);
+ }
+
+ // Listen for the window our prompt opens, so we can close it programatically
+ let promptWindow;
+ let windowListener = {
+ onOpenWindow(xulWindow) {
+ let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function listener() {
+ win.removeEventListener("load", listener, false);
+ if (win.document.documentElement.getAttribute("id") != WINDOW_ID) {
+ return;
+ }
+ // Found the window
+ promptWindow = win;
+ Services.wm.removeListener(windowListener);
+ }, false);
+ },
+ onCloseWindow() {},
+ onWindowTitleChange() {}
+ };
+ Services.wm.addListener(windowListener);
+
+ // |openDialog| is typically a blocking API, so |executeSoon| to get around this
+ DevToolsUtils.executeSoon(() => {
+ // Height determines the size of the QR code. Force a minimum size to
+ // improve scanability.
+ const MIN_HEIGHT = 600;
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ let width = win.outerWidth * 0.8;
+ let height = Math.max(win.outerHeight * 0.5, MIN_HEIGHT);
+ win.openDialog("chrome://webide/content/wifi-auth.xhtml",
+ WINDOW_ID,
+ "modal=yes,width=" + width + ",height=" + height, session);
+ });
+
+ return {
+ close() {
+ if (!promptWindow) {
+ return;
+ }
+ promptWindow.close();
+ promptWindow = null;
+ }
+ };
+ }
+};
+
+// For testing use only
+exports._WiFiRuntime = WiFiRuntime;
+
+function SimulatorRuntime(simulator) {
+ this.simulator = simulator;
+}
+
+SimulatorRuntime.prototype = {
+ type: RuntimeTypes.SIMULATOR,
+ connect: function (connection) {
+ return this.simulator.launch().then(port => {
+ connection.host = "localhost";
+ connection.port = port;
+ connection.keepConnecting = true;
+ connection.once(Connection.Events.DISCONNECTED, e => this.simulator.kill());
+ connection.connect();
+ });
+ },
+ configure() {
+ Simulators.emit("configure", this.simulator);
+ },
+ get id() {
+ return this.simulator.id;
+ },
+ get name() {
+ return this.simulator.name;
+ },
+};
+
+// For testing use only
+exports._SimulatorRuntime = SimulatorRuntime;
+
+var gLocalRuntime = {
+ type: RuntimeTypes.LOCAL,
+ connect: function (connection) {
+ if (!DebuggerServer.initialized) {
+ DebuggerServer.init();
+ DebuggerServer.addBrowserActors();
+ }
+ DebuggerServer.allowChromeProcess = true;
+ connection.host = null; // Force Pipe transport
+ connection.port = null;
+ connection.connect();
+ return promise.resolve();
+ },
+ get id() {
+ return "local";
+ },
+ get name() {
+ return Strings.GetStringFromName("local_runtime");
+ },
+};
+
+// For testing use only
+exports._gLocalRuntime = gLocalRuntime;
+
+var gRemoteRuntime = {
+ type: RuntimeTypes.REMOTE,
+ connect: function (connection) {
+ let win = Services.wm.getMostRecentWindow("devtools:webide");
+ if (!win) {
+ return promise.reject(new Error("No WebIDE window found"));
+ }
+ let ret = {value: connection.host + ":" + connection.port};
+ let title = Strings.GetStringFromName("remote_runtime_promptTitle");
+ let message = Strings.GetStringFromName("remote_runtime_promptMessage");
+ let ok = Services.prompt.prompt(win, title, message, ret, null, {});
+ let [host, port] = ret.value.split(":");
+ if (!ok) {
+ return promise.reject({canceled: true});
+ }
+ if (!host || !port) {
+ return promise.reject(new Error("Invalid host or port"));
+ }
+ connection.host = host;
+ connection.port = port;
+ connection.connect();
+ return promise.resolve();
+ },
+ get name() {
+ return Strings.GetStringFromName("remote_runtime");
+ },
+};
+
+// For testing use only
+exports._gRemoteRuntime = gRemoteRuntime;
diff --git a/devtools/client/webide/modules/simulator-process.js b/devtools/client/webide/modules/simulator-process.js
new file mode 100644
index 0000000000..7d0b57cc6c
--- /dev/null
+++ b/devtools/client/webide/modules/simulator-process.js
@@ -0,0 +1,325 @@
+/* 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 { Cc, Ci, Cu } = require("chrome");
+
+const Environment = require("sdk/system/environment").env;
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Subprocess = require("sdk/system/child_process/subprocess");
+const Services = require("Services");
+
+loader.lazyGetter(this, "OS", () => {
+ const Runtime = require("sdk/system/runtime");
+ switch (Runtime.OS) {
+ case "Darwin":
+ return "mac64";
+ case "Linux":
+ if (Runtime.XPCOMABI.indexOf("x86_64") === 0) {
+ return "linux64";
+ } else {
+ return "linux32";
+ }
+ case "WINNT":
+ return "win32";
+ default:
+ return "";
+ }
+});
+
+function SimulatorProcess() {}
+SimulatorProcess.prototype = {
+
+ // Check if B2G is running.
+ get isRunning() {
+ return !!this.process;
+ },
+
+ // Start the process and connect the debugger client.
+ run() {
+
+ // Resolve B2G binary.
+ let b2g = this.b2gBinary;
+ if (!b2g || !b2g.exists()) {
+ throw Error("B2G executable not found.");
+ }
+
+ // Ensure Gaia profile exists.
+ let gaia = this.gaiaProfile;
+ if (!gaia || !gaia.exists()) {
+ throw Error("Gaia profile directory not found.");
+ }
+
+ this.once("stdout", function () {
+ if (OS == "mac64") {
+ console.debug("WORKAROUND run osascript to show b2g-desktop window on OS=='mac64'");
+ // Escape double quotes and escape characters for use in AppleScript.
+ let path = b2g.path.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
+
+ Subprocess.call({
+ command: "/usr/bin/osascript",
+ arguments: ["-e", 'tell application "' + path + '" to activate'],
+ });
+ }
+ });
+
+ let logHandler = (e, data) => this.log(e, data.trim());
+ this.on("stdout", logHandler);
+ this.on("stderr", logHandler);
+ this.once("exit", () => {
+ this.off("stdout", logHandler);
+ this.off("stderr", logHandler);
+ });
+
+ let environment;
+ if (OS.indexOf("linux") > -1) {
+ environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path];
+ ["DISPLAY", "XAUTHORITY"].forEach(key => {
+ if (key in Environment) {
+ environment.push(key + "=" + Environment[key]);
+ }
+ });
+ }
+
+ // Spawn a B2G instance.
+ this.process = Subprocess.call({
+ command: b2g,
+ arguments: this.args,
+ environment: environment,
+ stdout: data => this.emit("stdout", data),
+ stderr: data => this.emit("stderr", data),
+ // On B2G instance exit, reset tracked process, remote debugger port and
+ // shuttingDown flag, then finally emit an exit event.
+ done: result => {
+ console.log("B2G terminated with " + result.exitCode);
+ this.process = null;
+ this.emit("exit", result.exitCode);
+ }
+ });
+ },
+
+ // Request a B2G instance kill.
+ kill() {
+ let deferred = promise.defer();
+ if (this.process) {
+ this.once("exit", (e, exitCode) => {
+ this.shuttingDown = false;
+ deferred.resolve(exitCode);
+ });
+ if (!this.shuttingDown) {
+ this.shuttingDown = true;
+ this.emit("kill", null);
+ this.process.kill();
+ }
+ return deferred.promise;
+ } else {
+ return promise.resolve(undefined);
+ }
+ },
+
+ // Maybe log output messages.
+ log(level, message) {
+ if (!Services.prefs.getBoolPref("devtools.webide.logSimulatorOutput")) {
+ return;
+ }
+ if (level === "stderr" || level === "error") {
+ console.error(message);
+ return;
+ }
+ console.log(message);
+ },
+
+ // Compute B2G CLI arguments.
+ get args() {
+ let args = [];
+
+ // Gaia profile.
+ args.push("-profile", this.gaiaProfile.path);
+
+ // Debugger server port.
+ let port = parseInt(this.options.port);
+ args.push("-start-debugger-server", "" + port);
+
+ // Screen size.
+ let width = parseInt(this.options.width);
+ let height = parseInt(this.options.height);
+ if (width && height) {
+ args.push("-screen", width + "x" + height);
+ }
+
+ // Ignore eventual zombie instances of b2g that are left over.
+ args.push("-no-remote");
+
+ // If we are running a simulator based on Mulet,
+ // we have to override the default chrome URL
+ // in order to prevent the Browser UI to appear.
+ if (this.b2gBinary.leafName.includes("firefox")) {
+ args.push("-chrome", "chrome://b2g/content/shell.html");
+ }
+
+ return args;
+ },
+};
+
+EventEmitter.decorate(SimulatorProcess.prototype);
+
+
+function CustomSimulatorProcess(options) {
+ this.options = options;
+}
+
+var CSPp = CustomSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(CSPp, "b2gBinary", {
+ get: function () {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.b2gBinary);
+ return file;
+ }
+});
+
+// Compute Gaia profile file handle.
+Object.defineProperty(CSPp, "gaiaProfile", {
+ get: function () {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.gaiaProfile);
+ return file;
+ }
+});
+
+exports.CustomSimulatorProcess = CustomSimulatorProcess;
+
+
+function AddonSimulatorProcess(addon, options) {
+ this.addon = addon;
+ this.options = options;
+}
+
+var ASPp = AddonSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(ASPp, "b2gBinary", {
+ get: function () {
+ let file;
+ try {
+ let pref = "extensions." + this.addon.id + ".customRuntime";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ } catch (e) {}
+
+ if (!file) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("b2g");
+ let binaries = {
+ win32: "b2g-bin.exe",
+ mac64: "B2G.app/Contents/MacOS/b2g-bin",
+ linux32: "b2g-bin",
+ linux64: "b2g-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ // If the binary doesn't exists, it may be because of a simulator
+ // based on mulet, which has a different binary name.
+ if (!file.exists()) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("firefox");
+ let binaries = {
+ win32: "firefox.exe",
+ mac64: "FirefoxNightly.app/Contents/MacOS/firefox-bin",
+ linux32: "firefox-bin",
+ linux64: "firefox-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ return file;
+ }
+});
+
+// Compute Gaia profile file handle.
+Object.defineProperty(ASPp, "gaiaProfile", {
+ get: function () {
+ let file;
+
+ // Custom profile from simulator configuration.
+ if (this.options.gaiaProfile) {
+ file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this.options.gaiaProfile);
+ return file;
+ }
+
+ // Custom profile from addon prefs.
+ try {
+ let pref = "extensions." + this.addon.id + ".gaiaProfile";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ return file;
+ } catch (e) {}
+
+ // Default profile from addon.
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ file.append("profile");
+ return file;
+ }
+});
+
+exports.AddonSimulatorProcess = AddonSimulatorProcess;
+
+
+function OldAddonSimulatorProcess(addon, options) {
+ this.addon = addon;
+ this.options = options;
+}
+
+var OASPp = OldAddonSimulatorProcess.prototype = Object.create(AddonSimulatorProcess.prototype);
+
+// Compute B2G binary file handle.
+Object.defineProperty(OASPp, "b2gBinary", {
+ get: function () {
+ let file;
+ try {
+ let pref = "extensions." + this.addon.id + ".customRuntime";
+ file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
+ } catch (e) {}
+
+ if (!file) {
+ file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
+ let version = this.addon.name.match(/\d+\.\d+/)[0].replace(/\./, "_");
+ file.append("resources");
+ file.append("fxos_" + version + "_simulator");
+ file.append("data");
+ file.append(OS == "linux32" ? "linux" : OS);
+ let binaries = {
+ win32: "b2g/b2g-bin.exe",
+ mac64: "B2G.app/Contents/MacOS/b2g-bin",
+ linux32: "b2g/b2g-bin",
+ linux64: "b2g/b2g-bin",
+ };
+ binaries[OS].split("/").forEach(node => file.append(node));
+ }
+ return file;
+ }
+});
+
+// Compute B2G CLI arguments.
+Object.defineProperty(OASPp, "args", {
+ get: function () {
+ let args = [];
+
+ // Gaia profile.
+ args.push("-profile", this.gaiaProfile.path);
+
+ // Debugger server port.
+ let port = parseInt(this.options.port);
+ args.push("-dbgport", "" + port);
+
+ // Ignore eventual zombie instances of b2g that are left over.
+ args.push("-no-remote");
+
+ return args;
+ }
+});
+
+exports.OldAddonSimulatorProcess = OldAddonSimulatorProcess;
diff --git a/devtools/client/webide/modules/simulators.js b/devtools/client/webide/modules/simulators.js
new file mode 100644
index 0000000000..f09df9e05c
--- /dev/null
+++ b/devtools/client/webide/modules/simulators.js
@@ -0,0 +1,368 @@
+/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
+const { Task } = require("devtools/shared/task");
+loader.lazyRequireGetter(this, "ConnectionManager", "devtools/shared/client/connection-manager", true);
+loader.lazyRequireGetter(this, "AddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+loader.lazyRequireGetter(this, "OldAddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+loader.lazyRequireGetter(this, "CustomSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
+const asyncStorage = require("devtools/shared/async-storage");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const Services = require("Services");
+
+const SimulatorRegExp = new RegExp(Services.prefs.getCharPref("devtools.webide.simulatorAddonRegExp"));
+const LocaleCompare = (a, b) => {
+ return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
+};
+
+var Simulators = {
+
+ // The list of simulator configurations.
+ _simulators: [],
+
+ /**
+ * Load a previously saved list of configurations (only once).
+ *
+ * @return Promise.
+ */
+ _load() {
+ if (this._loadingPromise) {
+ return this._loadingPromise;
+ }
+
+ this._loadingPromise = Task.spawn(function* () {
+ let jobs = [];
+
+ let value = yield asyncStorage.getItem("simulators");
+ if (Array.isArray(value)) {
+ value.forEach(options => {
+ let simulator = new Simulator(options);
+ Simulators.add(simulator, true);
+
+ // If the simulator had a reference to an addon, fix it.
+ if (options.addonID) {
+ let deferred = promise.defer();
+ AddonManager.getAddonByID(options.addonID, addon => {
+ simulator.addon = addon;
+ delete simulator.options.addonID;
+ deferred.resolve();
+ });
+ jobs.push(deferred.promise);
+ }
+ });
+ }
+
+ yield promise.all(jobs);
+ yield Simulators._addUnusedAddons();
+ Simulators.emitUpdated();
+ return Simulators._simulators;
+ });
+
+ return this._loadingPromise;
+ },
+
+ /**
+ * Add default simulators to the list for each new (unused) addon.
+ *
+ * @return Promise.
+ */
+ _addUnusedAddons: Task.async(function* () {
+ let jobs = [];
+
+ let addons = yield Simulators.findSimulatorAddons();
+ addons.forEach(addon => {
+ jobs.push(Simulators.addIfUnusedAddon(addon, true));
+ });
+
+ yield promise.all(jobs);
+ }),
+
+ /**
+ * Save the current list of configurations.
+ *
+ * @return Promise.
+ */
+ _save: Task.async(function* () {
+ yield this._load();
+
+ let value = Simulators._simulators.map(simulator => {
+ let options = JSON.parse(JSON.stringify(simulator.options));
+ if (simulator.addon != null) {
+ options.addonID = simulator.addon.id;
+ }
+ return options;
+ });
+
+ yield asyncStorage.setItem("simulators", value);
+ }),
+
+ /**
+ * List all available simulators.
+ *
+ * @return Promised simulator list.
+ */
+ findSimulators: Task.async(function* () {
+ yield this._load();
+ return Simulators._simulators;
+ }),
+
+ /**
+ * List all installed simulator addons.
+ *
+ * @return Promised addon list.
+ */
+ findSimulatorAddons() {
+ let deferred = promise.defer();
+ AddonManager.getAllAddons(all => {
+ let addons = [];
+ for (let addon of all) {
+ if (Simulators.isSimulatorAddon(addon)) {
+ addons.push(addon);
+ }
+ }
+ // Sort simulator addons by name.
+ addons.sort(LocaleCompare);
+ deferred.resolve(addons);
+ });
+ return deferred.promise;
+ },
+
+ /**
+ * Add a new simulator for `addon` if no other simulator uses it.
+ */
+ addIfUnusedAddon(addon, silently = false) {
+ let simulators = this._simulators;
+ let matching = simulators.filter(s => s.addon && s.addon.id == addon.id);
+ if (matching.length > 0) {
+ return promise.resolve();
+ }
+ let options = {};
+ options.name = addon.name.replace(" Simulator", "");
+ // Some addons specify a simulator type at the end of their version string,
+ // e.g. "2_5_tv".
+ let type = this.simulatorAddonVersion(addon).split("_")[2];
+ if (type) {
+ // "tv" is shorthand for type "television".
+ options.type = (type === "tv" ? "television" : type);
+ }
+ return this.add(new Simulator(options, addon), silently);
+ },
+
+ // TODO (Bug 1146521) Maybe find a better way to deal with removed addons?
+ removeIfUsingAddon(addon) {
+ let simulators = this._simulators;
+ let remaining = simulators.filter(s => !s.addon || s.addon.id != addon.id);
+ this._simulators = remaining;
+ if (remaining.length !== simulators.length) {
+ this.emitUpdated();
+ }
+ },
+
+ /**
+ * Add a new simulator to the list. Caution: `simulator.name` may be modified.
+ *
+ * @return Promise to added simulator.
+ */
+ add(simulator, silently = false) {
+ let simulators = this._simulators;
+ let uniqueName = this.uniqueName(simulator.options.name);
+ simulator.options.name = uniqueName;
+ simulators.push(simulator);
+ if (!silently) {
+ this.emitUpdated();
+ }
+ return promise.resolve(simulator);
+ },
+
+ /**
+ * Remove a simulator from the list.
+ */
+ remove(simulator) {
+ let simulators = this._simulators;
+ let remaining = simulators.filter(s => s !== simulator);
+ this._simulators = remaining;
+ if (remaining.length !== simulators.length) {
+ this.emitUpdated();
+ }
+ },
+
+ /**
+ * Get a unique name for a simulator (may add a suffix, e.g. "MyName (1)").
+ */
+ uniqueName(name) {
+ let simulators = this._simulators;
+
+ let names = {};
+ simulators.forEach(simulator => names[simulator.name] = true);
+
+ // Strip any previous suffix, add a new suffix if necessary.
+ let stripped = name.replace(/ \(\d+\)$/, "");
+ let unique = stripped;
+ for (let i = 1; names[unique]; i++) {
+ unique = stripped + " (" + i + ")";
+ }
+ return unique;
+ },
+
+ /**
+ * Compare an addon's ID against the expected form of a simulator addon ID,
+ * and try to extract its version if there is a match.
+ *
+ * Note: If a simulator addon is recognized, but no version can be extracted
+ * (e.g. custom RegExp pref value), we return "Unknown" to keep the returned
+ * value 'truthy'.
+ */
+ simulatorAddonVersion(addon) {
+ let match = SimulatorRegExp.exec(addon.id);
+ if (!match) {
+ return null;
+ }
+ let version = match[1];
+ return version || "Unknown";
+ },
+
+ /**
+ * Detect simulator addons, including "unofficial" ones.
+ */
+ isSimulatorAddon(addon) {
+ return !!this.simulatorAddonVersion(addon);
+ },
+
+ emitUpdated() {
+ this.emit("updated", { length: this._simulators.length });
+ this._simulators.sort(LocaleCompare);
+ this._save();
+ },
+
+ onConfigure(e, simulator) {
+ this._lastConfiguredSimulator = simulator;
+ },
+
+ onInstalled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.addIfUnusedAddon(addon);
+ }
+ },
+
+ onEnabled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.addIfUnusedAddon(addon);
+ }
+ },
+
+ onDisabled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.removeIfUsingAddon(addon);
+ }
+ },
+
+ onUninstalled(addon) {
+ if (this.isSimulatorAddon(addon)) {
+ this.removeIfUsingAddon(addon);
+ }
+ },
+};
+exports.Simulators = Simulators;
+AddonManager.addAddonListener(Simulators);
+EventEmitter.decorate(Simulators);
+Simulators.on("configure", Simulators.onConfigure.bind(Simulators));
+
+function Simulator(options = {}, addon = null) {
+ this.addon = addon;
+ this.options = options;
+
+ // Fill `this.options` with default values where needed.
+ let defaults = this.defaults;
+ for (let option in defaults) {
+ if (this.options[option] == null) {
+ this.options[option] = defaults[option];
+ }
+ }
+}
+Simulator.prototype = {
+
+ // Default simulation options.
+ _defaults: {
+ // Based on the Firefox OS Flame.
+ phone: {
+ width: 320,
+ height: 570,
+ pixelRatio: 1.5
+ },
+ // Based on a 720p HD TV.
+ television: {
+ width: 1280,
+ height: 720,
+ pixelRatio: 1,
+ }
+ },
+ _defaultType: "phone",
+
+ restoreDefaults() {
+ let defaults = this.defaults;
+ let options = this.options;
+ for (let option in defaults) {
+ options[option] = defaults[option];
+ }
+ },
+
+ launch() {
+ // Close already opened simulation.
+ if (this.process) {
+ return this.kill().then(this.launch.bind(this));
+ }
+
+ this.options.port = ConnectionManager.getFreeTCPPort();
+
+ // Choose simulator process type.
+ if (this.options.b2gBinary) {
+ // Custom binary.
+ this.process = new CustomSimulatorProcess(this.options);
+ } else if (this.version > "1.3") {
+ // Recent simulator addon.
+ this.process = new AddonSimulatorProcess(this.addon, this.options);
+ } else {
+ // Old simulator addon.
+ this.process = new OldAddonSimulatorProcess(this.addon, this.options);
+ }
+ this.process.run();
+
+ return promise.resolve(this.options.port);
+ },
+
+ kill() {
+ let process = this.process;
+ if (!process) {
+ return promise.resolve();
+ }
+ this.process = null;
+ return process.kill();
+ },
+
+ get defaults() {
+ let defaults = this._defaults;
+ return defaults[this.type] || defaults[this._defaultType];
+ },
+
+ get id() {
+ return this.name;
+ },
+
+ get name() {
+ return this.options.name;
+ },
+
+ get type() {
+ return this.options.type || this._defaultType;
+ },
+
+ get version() {
+ return this.options.b2gBinary ? "Custom" : this.addon.name.match(/\d+\.\d+/)[0];
+ },
+};
+exports.Simulator = Simulator;
diff --git a/devtools/client/webide/modules/tab-store.js b/devtools/client/webide/modules/tab-store.js
new file mode 100644
index 0000000000..0fed366ccc
--- /dev/null
+++ b/devtools/client/webide/modules/tab-store.js
@@ -0,0 +1,178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Cu } = require("chrome");
+
+const { TargetFactory } = require("devtools/client/framework/target");
+const EventEmitter = require("devtools/shared/event-emitter");
+const { Connection } = require("devtools/shared/client/connection-manager");
+const promise = require("promise");
+const { Task } = require("devtools/shared/task");
+
+const _knownTabStores = new WeakMap();
+
+var TabStore;
+
+module.exports = TabStore = function (connection) {
+ // If we already know about this connection,
+ // let's re-use the existing store.
+ if (_knownTabStores.has(connection)) {
+ return _knownTabStores.get(connection);
+ }
+
+ _knownTabStores.set(connection, this);
+
+ EventEmitter.decorate(this);
+
+ this._resetStore();
+
+ this.destroy = this.destroy.bind(this);
+ this._onStatusChanged = this._onStatusChanged.bind(this);
+
+ this._connection = connection;
+ this._connection.once(Connection.Events.DESTROYED, this.destroy);
+ this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+ this._onTabListChanged = this._onTabListChanged.bind(this);
+ this._onTabNavigated = this._onTabNavigated.bind(this);
+ this._onStatusChanged();
+ return this;
+};
+
+TabStore.prototype = {
+
+ destroy: function () {
+ if (this._connection) {
+ // While this.destroy is bound using .once() above, that event may not
+ // have occurred when the TabStore client calls destroy, so we
+ // manually remove it here.
+ this._connection.off(Connection.Events.DESTROYED, this.destroy);
+ this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
+ _knownTabStores.delete(this._connection);
+ this._connection = null;
+ }
+ },
+
+ _resetStore: function () {
+ this.response = null;
+ this.tabs = [];
+ this._selectedTab = null;
+ this._selectedTabTargetPromise = null;
+ },
+
+ _onStatusChanged: function () {
+ if (this._connection.status == Connection.Status.CONNECTED) {
+ // Watch for changes to remote browser tabs
+ this._connection.client.addListener("tabListChanged",
+ this._onTabListChanged);
+ this._connection.client.addListener("tabNavigated",
+ this._onTabNavigated);
+ this.listTabs();
+ } else {
+ if (this._connection.client) {
+ this._connection.client.removeListener("tabListChanged",
+ this._onTabListChanged);
+ this._connection.client.removeListener("tabNavigated",
+ this._onTabNavigated);
+ }
+ this._resetStore();
+ }
+ },
+
+ _onTabListChanged: function () {
+ this.listTabs().then(() => this.emit("tab-list"))
+ .catch(console.error);
+ },
+
+ _onTabNavigated: function (e, { from, title, url }) {
+ if (!this._selectedTab || from !== this._selectedTab.actor) {
+ return;
+ }
+ this._selectedTab.url = url;
+ this._selectedTab.title = title;
+ this.emit("navigate");
+ },
+
+ listTabs: function () {
+ if (!this._connection || !this._connection.client) {
+ return promise.reject(new Error("Can't listTabs, not connected."));
+ }
+ let deferred = promise.defer();
+ this._connection.client.listTabs(response => {
+ if (response.error) {
+ this._connection.disconnect();
+ deferred.reject(response.error);
+ return;
+ }
+ let tabsChanged = JSON.stringify(this.tabs) !== JSON.stringify(response.tabs);
+ this.response = response;
+ this.tabs = response.tabs;
+ this._checkSelectedTab();
+ if (tabsChanged) {
+ this.emit("tab-list");
+ }
+ deferred.resolve(response);
+ });
+ return deferred.promise;
+ },
+
+ // TODO: Tab "selection" should really take place by creating a TabProject
+ // which is the selected project. This should be done as part of the
+ // project-agnostic work.
+ _selectedTab: null,
+ _selectedTabTargetPromise: null,
+ get selectedTab() {
+ return this._selectedTab;
+ },
+ set selectedTab(tab) {
+ if (this._selectedTab === tab) {
+ return;
+ }
+ this._selectedTab = tab;
+ this._selectedTabTargetPromise = null;
+ // Attach to the tab to follow navigation events
+ if (this._selectedTab) {
+ this.getTargetForTab();
+ }
+ },
+
+ _checkSelectedTab: function () {
+ if (!this._selectedTab) {
+ return;
+ }
+ let alive = this.tabs.some(tab => {
+ return tab.actor === this._selectedTab.actor;
+ });
+ if (!alive) {
+ this._selectedTab = null;
+ this._selectedTabTargetPromise = null;
+ this.emit("closed");
+ }
+ },
+
+ getTargetForTab: function () {
+ if (this._selectedTabTargetPromise) {
+ return this._selectedTabTargetPromise;
+ }
+ let store = this;
+ this._selectedTabTargetPromise = Task.spawn(function* () {
+ // If you connect to a tab, then detach from it, the root actor may have
+ // de-listed the actors that belong to the tab. This breaks the toolbox
+ // if you try to connect to the same tab again. To work around this
+ // issue, we force a "listTabs" request before connecting to a tab.
+ yield store.listTabs();
+ return TargetFactory.forRemoteTab({
+ form: store._selectedTab,
+ client: store._connection.client,
+ chrome: false
+ });
+ });
+ this._selectedTabTargetPromise.then(target => {
+ target.once("close", () => {
+ this._selectedTabTargetPromise = null;
+ });
+ });
+ return this._selectedTabTargetPromise;
+ },
+
+};
diff --git a/devtools/client/webide/modules/utils.js b/devtools/client/webide/modules/utils.js
new file mode 100644
index 0000000000..7a19c70440
--- /dev/null
+++ b/devtools/client/webide/modules/utils.js
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { Cc, Cu, Ci } = require("chrome");
+const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const Services = require("Services");
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+function doesFileExist(location) {
+ let file = new FileUtils.File(location);
+ return file.exists();
+}
+exports.doesFileExist = doesFileExist;
+
+function _getFile(location, ...pickerParams) {
+ if (location) {
+ return new FileUtils.File(location);
+ }
+ let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(...pickerParams);
+ let res = fp.show();
+ if (res == Ci.nsIFilePicker.returnCancel) {
+ return null;
+ }
+ return fp.file;
+}
+
+function getCustomBinary(window, location) {
+ return _getFile(location, window, Strings.GetStringFromName("selectCustomBinary_title"), Ci.nsIFilePicker.modeOpen);
+}
+exports.getCustomBinary = getCustomBinary;
+
+function getCustomProfile(window, location) {
+ return _getFile(location, window, Strings.GetStringFromName("selectCustomProfile_title"), Ci.nsIFilePicker.modeGetFolder);
+}
+exports.getCustomProfile = getCustomProfile;
+
+function getPackagedDirectory(window, location) {
+ return _getFile(location, window, Strings.GetStringFromName("importPackagedApp_title"), Ci.nsIFilePicker.modeGetFolder);
+}
+exports.getPackagedDirectory = getPackagedDirectory;
+
+function getHostedURL(window, location) {
+ let ret = { value: null };
+
+ if (!location) {
+ Services.prompt.prompt(window,
+ Strings.GetStringFromName("importHostedApp_title"),
+ Strings.GetStringFromName("importHostedApp_header"),
+ ret, null, {});
+ location = ret.value;
+ }
+
+ if (!location) {
+ return null;
+ }
+
+ // Clean location string and add "http://" if missing
+ location = location.trim();
+ try { // Will fail if no scheme
+ Services.io.extractScheme(location);
+ } catch (e) {
+ location = "http://" + location;
+ }
+ return location;
+}
+exports.getHostedURL = getHostedURL;
diff --git a/devtools/client/webide/moz.build b/devtools/client/webide/moz.build
new file mode 100644
index 0000000000..c5dcb07a91
--- /dev/null
+++ b/devtools/client/webide/moz.build
@@ -0,0 +1,23 @@
+# -*- 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/.
+
+DIRS += [
+ 'content',
+ 'components',
+ 'modules',
+ 'themes',
+]
+
+BROWSER_CHROME_MANIFESTS += [
+ 'test/browser.ini'
+]
+MOCHITEST_CHROME_MANIFESTS += [
+ 'test/chrome.ini'
+]
+
+JS_PREFERENCE_PP_FILES += [
+ 'webide-prefs.js',
+]
diff --git a/devtools/client/webide/test/.eslintrc.js b/devtools/client/webide/test/.eslintrc.js
new file mode 100644
index 0000000000..8d15a76d9b
--- /dev/null
+++ b/devtools/client/webide/test/.eslintrc.js
@@ -0,0 +1,6 @@
+"use strict";
+
+module.exports = {
+ // Extend from the shared list of defined globals for mochitests.
+ "extends": "../../../.eslintrc.mochitests.js"
+};
diff --git a/devtools/client/webide/test/addons/adbhelper-linux.xpi b/devtools/client/webide/test/addons/adbhelper-linux.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zcmWIWW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zOymE!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID8^o0n-^c@aMV8l$DzpRzXbjF;(pZ#hMZKB1-{f~F8mzVr4
z^n1<5_2TXdE@CX*`KPTvJkBncmAf(ftlF&}>rWYGvyXa8Enm4|lStlj$?Z@vaFo
zkN9)WYfa2~O9sdDH|!@^r>-=)eeUi;ty|)cZZG?|z2C;9B=4l4wAw%KqoGEAr&elQ
zVmbPxD5xuP-UNx>!XD#UzQ@BS*QblgT-|!fo^SE;>48m)`%0VUZ7#IlusrZmas0;Q
z^&e-aRmZZFz;-*W%5>K8BmTu+~{<=gA0yJ|k|dHvw5O81TT4q|`U1H2iTUR8jO;Dfd+!oFNT4vY#_^-fN(L84gn^8
G1_l5Om?2^S
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/adbhelper-linux64.xpi b/devtools/client/webide/test/addons/adbhelper-linux64.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zcmWIWW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zOymE!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID8^o0n-^c@aMV8l$DzpRzXbjF;(pZ#hMZKB1-{f~F8mzVr4
z^n1<5_2TXdE@CX*`KPTvJkBncmAf(ftlF&}>rWYGvyXa8Enm4|lStlj$?Z@vaFo
zkN9)WYfa2~O9sdDH|!@^r>-=)eeUi;ty|)cZZG?|z2C;9B=4l4wAw%KqoGEAr&elQ
zVmbPxD5xuP-UNx>!XD#UzQ@BS*QblgT-|!fo^SE;>48m)`%0VUZ7#IlusrZmas0;Q
z^&e-aRmZZFz;-*W%5>K8BmTu+~{<=gA0yJ|k|dHvw5O81TT4q|`U1H2iTUR8jO;Dfd+!oFNT4vY#_^-fN(L84gn^8
G1_l5Om?2^S
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/adbhelper-mac64.xpi b/devtools/client/webide/test/addons/adbhelper-mac64.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zcmWIWW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zOymE!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID8^o0n-^c@aMV8l$DzpRzXbjF;(pZ#hMZKB1-{f~F8mzVr4
z^n1<5_2TXdE@CX*`KPTvJkBncmAf(ftlF&}>rWYGvyXa8Enm4|lStlj$?Z@vaFo
zkN9)WYfa2~O9sdDH|!@^r>-=)eeUi;ty|)cZZG?|z2C;9B=4l4wAw%KqoGEAr&elQ
zVmbPxD5xuP-UNx>!XD#UzQ@BS*QblgT-|!fo^SE;>48m)`%0VUZ7#IlusrZmas0;Q
z^&e-aRmZZFz;-*W%5>K8BmTu+~{<=gA0yJ|k|dHvw5O81TT4q|`U1H2iTUR8jO;Dfd+!oFNT4vY#_^-fN(L84gn^8
G1_l5Om?2^S
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/adbhelper-win32.xpi b/devtools/client/webide/test/addons/adbhelper-win32.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..b56cc03e349e4b871ea95b61ceeb53d6969759ab
GIT binary patch
literal 1293
zcmWIWW@Zs#U|`^2U~Ag!az0|k)eVdc3<1mx3_J`n3`zOymE!`ukb75}0U3>gB%jVo$mbX(P9^Es0GBeC#
z$_~x@Q+K>hxX|+Px%b&`a_)yeI9`aFmQn7x;^;-M^$kxwmu>bl-PhW3tfFPE#Om4V
z5{~*t>sDvYsGhF)PW$SbEzzNViJR0a#O_4pn4C1onklZgHvIM6%#PRTO0mM8Yc3x?
z^K!>LyGxFXKV&)uWr}u9(pUOe>2j-H-qUu=Nx^VkSrwM!SC_7;d6F1-W%DJowu44%
zk5~S;Kk9ukGNX0*MsYZaMu-KCl?ggq7&
z=Xm81^kv7H1#^OSGe5Pn48M~8Y{_ep&swhYm+-p(pDuB7|I-aC{?FZJ+_&;r=;bH(
z=e*AkfX6y-<7Ss8Hp56sCI$vJ76t}xV61266_+ID8^o0n-^c@aMV8l$DzpRzXbjF;(pZ#hMZKB1-{f~F8mzVr4
z^n1<5_2TXdE@CX*`KPTvJkBncmAf(ftlF&}>rWYGvyXa8Enm4|lStlj$?Z@vaFo
zkN9)WYfa2~O9sdDH|!@^r>-=)eeUi;ty|)cZZG?|z2C;9B=4l4wAw%KqoGEAr&elQ
zVmbPxD5xuP-UNx>!XD#UzQ@BS*QblgT-|!fo^SE;>48m)`%0VUZ7#IlusrZmas0;Q
z^&e-aRmZZFz;-*W%5>K8BmTu+~{<=gA0yJ|k|dHvw5O81TT4q|`U1H2iTUR8jO;Dfd+!oFNT4vY#_^-fN(L84gn^8
G1_l5Om?2^S
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi b/devtools/client/webide/test/addons/fxdt-adapters-linux32.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..5a512ae3d199746350c650e527b0178411b0440e
GIT binary patch
literal 1156
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
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-linux64.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..2f86c4d4d5f68a212acf5e59cdb9d5f7a5cb60a5
GIT binary patch
literal 5046
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
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_1_0_simulator-mac64.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..6da2fcbad77aeb90b39898a5cb005d6f2f8d6f93
GIT binary patch
literal 5044
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
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..e2335e3a0462f1344d412552263b43345bda9d4b
GIT binary patch
literal 5046
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
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-linux64.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..75fe209eac3f6c16523e57976464e1db50d9b521
GIT binary patch
literal 5046
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
literal 0
HcmV?d00001
diff --git a/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi b/devtools/client/webide/test/addons/fxos_2_0_simulator-mac64.xpi
new file mode 100644
index 0000000000000000000000000000000000000000..58749f72421d1fc04c91a08672c8d907eb314d48
GIT binary patch
literal 5043
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