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_aboutaddons; + &deck_close; +
+ +

&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_valid_header; + &details_warning_header; + &details_error_header; +

+
+ +
+ +
+

+

+
+
+ +
+

&details_description;

+

+ +

&details_location;

+

+ +

&details_manifestURL;

+

+ + +
+ + + + + + 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; +]> + + + + + + + + + +
+ +

&devicepreference_title;

+ +
+ + + + + + +
+ + + + + + +
+ + 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; +]> + + + + + + + + + +
+ +

&devicesetting_title;

+ +
+ + + + + + +
+ + + + + + +
+ + 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; +]> + + + + + + + + + + + + +
+ &deck_close; +
+ +

&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_help; + &deck_close; +
+ +

&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; +]> + + + + + + + + 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; +]> + + + + + + + + + + +
+ &deck_close; +
+ +

&permissionstable_title;

+ + + + + + + + +
&permissionstable_name_header;type:webtype:privilegedtype: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_restore; + &prefs_manage_components; + &deck_close; +
+ +

&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; +]> + + + + + + + + +
+
+ + + + +
+ +
+ +
+
+
+ + 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; +]> + + + + + + + + +
+
+ + + +
+ +
+ +
+ + +
+
+ + + + + + +
+
+
+ + 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_ADBRootWarning; +
+

+

+ &runtimedetails_unrestrictedPrivileges; +

+ + &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
element used to edit Simulator options. + _form: null, + + // The Simulator object being edited. + _simulator: null, + + // Generate the dynamic form elements. + init() { + let promises = []; + + // Grab the element. + let form = this._form; + if (!form) { + // This is the first time we run `init()`, bootstrap some things. + form = this._form = document.querySelector("#simulator-editor"); + form.addEventListener("change", this.update.bind(this)); + Simulators.on("configure", (e, simulator) => { this.edit(simulator); }); + // Extract the list of device simulation options we'll support. + let deviceFields = form.querySelectorAll("*[data-device]"); + this._deviceOptions = Array.map(deviceFields, field => field.name); + } + + // Append a new
+ + + 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; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+