summaryrefslogtreecommitdiffstats
path: root/devtools/client/webide/content/webide.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webide/content/webide.js')
-rw-r--r--devtools/client/webide/content/webide.js1157
1 files changed, 1157 insertions, 0 deletions
diff --git a/devtools/client/webide/content/webide.js b/devtools/client/webide/content/webide.js
new file mode 100644
index 000000000..c222332e3
--- /dev/null
+++ b/devtools/client/webide/content/webide.js
@@ -0,0 +1,1157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Cu = Components.utils;
+var Ci = Components.interfaces;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {gDevToolsBrowser} = require("devtools/client/framework/devtools-browser");
+const {Toolbox} = require("devtools/client/framework/toolbox");
+const Services = require("Services");
+const {AppProjects} = require("devtools/client/webide/modules/app-projects");
+const {Connection} = require("devtools/shared/client/connection-manager");
+const {AppManager} = require("devtools/client/webide/modules/app-manager");
+const EventEmitter = require("devtools/shared/event-emitter");
+const promise = require("promise");
+const ProjectEditor = require("devtools/client/projecteditor/lib/projecteditor");
+const {GetAvailableAddons} = require("devtools/client/webide/modules/addons");
+const {getJSON} = require("devtools/client/shared/getjson");
+const utils = require("devtools/client/webide/modules/utils");
+const Telemetry = require("devtools/client/shared/telemetry");
+const {RuntimeScanners} = require("devtools/client/webide/modules/runtimes");
+const {showDoorhanger} = require("devtools/client/shared/doorhanger");
+const {Simulators} = require("devtools/client/webide/modules/simulators");
+const {Task} = require("devtools/shared/task");
+
+const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
+
+const HTML = "http://www.w3.org/1999/xhtml";
+const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshooting";
+
+const MAX_ZOOM = 1.4;
+const MIN_ZOOM = 0.6;
+
+const MS_PER_DAY = 86400000;
+
+[["AppManager", AppManager],
+ ["AppProjects", AppProjects],
+ ["Connection", Connection]].forEach(([key, value]) => {
+ Object.defineProperty(this, key, {
+ value: value,
+ enumerable: true,
+ writable: false
+ });
+ });
+
+// Download remote resources early
+getJSON("devtools.webide.addonsURL");
+getJSON("devtools.webide.templatesURL");
+getJSON("devtools.devices.url");
+
+// See bug 989619
+console.log = console.log.bind(console);
+console.warn = console.warn.bind(console);
+console.error = console.error.bind(console);
+
+window.addEventListener("load", function onLoad() {
+ window.removeEventListener("load", onLoad);
+ UI.init();
+});
+
+window.addEventListener("unload", function onUnload() {
+ window.removeEventListener("unload", onUnload);
+ UI.destroy();
+});
+
+var UI = {
+ init: function () {
+ this._telemetry = new Telemetry();
+ this._telemetry.toolOpened("webide");
+
+ AppManager.init();
+
+ this.appManagerUpdate = this.appManagerUpdate.bind(this);
+ AppManager.on("app-manager-update", this.appManagerUpdate);
+
+ Cmds.showProjectPanel();
+ Cmds.showRuntimePanel();
+
+ this.updateCommands();
+
+ this.onfocus = this.onfocus.bind(this);
+ window.addEventListener("focus", this.onfocus, true);
+
+ AppProjects.load().then(() => {
+ this.autoSelectProject();
+ }, e => {
+ console.error(e);
+ this.reportError("error_appProjectsLoadFailed");
+ });
+
+ // Auto install the ADB Addon Helper and Tools Adapters. Only once.
+ // If the user decides to uninstall any of this addon, we won't install it again.
+ let autoinstallADBHelper = Services.prefs.getBoolPref("devtools.webide.autoinstallADBHelper");
+ let autoinstallFxdtAdapters = Services.prefs.getBoolPref("devtools.webide.autoinstallFxdtAdapters");
+ if (autoinstallADBHelper) {
+ GetAvailableAddons().then(addons => {
+ addons.adb.install();
+ }, console.error);
+ }
+ if (autoinstallFxdtAdapters) {
+ GetAvailableAddons().then(addons => {
+ addons.adapters.install();
+ }, console.error);
+ }
+ Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
+ Services.prefs.setBoolPref("devtools.webide.autoinstallFxdtAdapters", false);
+
+ if (Services.prefs.getBoolPref("devtools.webide.widget.autoinstall") &&
+ !Services.prefs.getBoolPref("devtools.webide.widget.enabled")) {
+ Services.prefs.setBoolPref("devtools.webide.widget.enabled", true);
+ gDevToolsBrowser.moveWebIDEWidgetInNavbar();
+ }
+
+ this.setupDeck();
+
+ this.contentViewer = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell)
+ .contentViewer;
+ this.contentViewer.fullZoom = Services.prefs.getCharPref("devtools.webide.zoom");
+
+ gDevToolsBrowser.isWebIDEInitialized.resolve();
+
+ this.configureSimulator = this.configureSimulator.bind(this);
+ Simulators.on("configure", this.configureSimulator);
+ },
+
+ destroy: function () {
+ window.removeEventListener("focus", this.onfocus, true);
+ AppManager.off("app-manager-update", this.appManagerUpdate);
+ AppManager.destroy();
+ Simulators.off("configure", this.configureSimulator);
+ this.updateConnectionTelemetry();
+ this._telemetry.toolClosed("webide");
+ this._telemetry.toolClosed("webideProjectEditor");
+ this._telemetry.destroy();
+ },
+
+ canCloseProject: function () {
+ if (this.projecteditor) {
+ return this.projecteditor.confirmUnsaved();
+ }
+ return true;
+ },
+
+ onfocus: function () {
+ // Because we can't track the activity in the folder project,
+ // we need to validate the project regularly. Let's assume that
+ // if a modification happened, it happened when the window was
+ // not focused.
+ if (AppManager.selectedProject &&
+ AppManager.selectedProject.type != "mainProcess" &&
+ AppManager.selectedProject.type != "runtimeApp" &&
+ AppManager.selectedProject.type != "tab") {
+ AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ }
+
+ // Hook to display promotional Developer Edition doorhanger. Only displayed once.
+ // Hooked into the `onfocus` event because sometimes does not work
+ // when run at the end of `init`. ¯\(°_o)/¯
+ showDoorhanger({ window, type: "deveditionpromo", anchor: document.querySelector("#deck") });
+ },
+
+ appManagerUpdate: function (event, what, details) {
+ // Got a message from app-manager.js
+ // See AppManager.update() for descriptions of what these events mean.
+ switch (what) {
+ case "runtime-list":
+ this.autoConnectRuntime();
+ break;
+ case "connection":
+ this.updateRuntimeButton();
+ this.updateCommands();
+ this.updateConnectionTelemetry();
+ break;
+ case "before-project":
+ if (!this.canCloseProject()) {
+ details.cancel();
+ }
+ break;
+ case "project":
+ this._updatePromise = Task.spawn(function* () {
+ UI.updateTitle();
+ yield UI.destroyToolbox();
+ UI.updateCommands();
+ UI.openProject();
+ yield UI.autoStartProject();
+ UI.autoOpenToolbox();
+ UI.saveLastSelectedProject();
+ UI.updateRemoveProjectButton();
+ });
+ return;
+ case "project-started":
+ this.updateCommands();
+ UI.autoOpenToolbox();
+ break;
+ case "project-stopped":
+ UI.destroyToolbox();
+ this.updateCommands();
+ break;
+ case "runtime-global-actors":
+ // Check runtime version only on runtime-global-actors,
+ // as we expect to use device actor
+ this.checkRuntimeVersion();
+ this.updateCommands();
+ break;
+ case "runtime-details":
+ this.updateRuntimeButton();
+ break;
+ case "runtime":
+ this.updateRuntimeButton();
+ this.saveLastConnectedRuntime();
+ break;
+ case "project-validated":
+ this.updateTitle();
+ this.updateCommands();
+ this.updateProjectEditorHeader();
+ break;
+ case "install-progress":
+ this.updateProgress(Math.round(100 * details.bytesSent / details.totalBytes));
+ break;
+ case "runtime-targets":
+ this.autoSelectProject();
+ break;
+ case "pre-package":
+ this.prePackageLog(details);
+ break;
+ }
+ this._updatePromise = promise.resolve();
+ },
+
+ configureSimulator: function (event, simulator) {
+ UI.selectDeckPanel("simulator");
+ },
+
+ openInBrowser: function (url) {
+ // Open a URL in a Firefox window
+ let mainWindow = Services.wm.getMostRecentWindow(gDevTools.chromeWindowType);
+ if (mainWindow) {
+ mainWindow.openUILinkIn(url, "tab");
+ mainWindow.focus()
+ } else {
+ window.open(url);
+ }
+ },
+
+ updateTitle: function () {
+ let project = AppManager.selectedProject;
+ if (project) {
+ window.document.title = Strings.formatStringFromName("title_app", [project.name], 1);
+ } else {
+ window.document.title = Strings.GetStringFromName("title_noApp");
+ }
+ },
+
+ /** ******** BUSY UI **********/
+
+ _busyTimeout: null,
+ _busyOperationDescription: null,
+ _busyPromise: null,
+
+ updateProgress: function (percent) {
+ let progress = document.querySelector("#action-busy-determined");
+ progress.mode = "determined";
+ progress.value = percent;
+ this.setupBusyTimeout();
+ },
+
+ busy: function () {
+ let win = document.querySelector("window");
+ win.classList.add("busy");
+ win.classList.add("busy-undetermined");
+ this.updateCommands();
+ this.update("busy");
+ },
+
+ unbusy: function () {
+ let win = document.querySelector("window");
+ win.classList.remove("busy");
+ win.classList.remove("busy-determined");
+ win.classList.remove("busy-undetermined");
+ this.updateCommands();
+ this.update("unbusy");
+ this._busyPromise = null;
+ },
+
+ setupBusyTimeout: function () {
+ this.cancelBusyTimeout();
+ this._busyTimeout = setTimeout(() => {
+ this.unbusy();
+ UI.reportError("error_operationTimeout", this._busyOperationDescription);
+ }, Services.prefs.getIntPref("devtools.webide.busyTimeout"));
+ },
+
+ cancelBusyTimeout: function () {
+ clearTimeout(this._busyTimeout);
+ },
+
+ busyWithProgressUntil: function (promise, operationDescription) {
+ let busy = this.busyUntil(promise, operationDescription);
+ let win = document.querySelector("window");
+ let progress = document.querySelector("#action-busy-determined");
+ progress.mode = "undetermined";
+ win.classList.add("busy-determined");
+ win.classList.remove("busy-undetermined");
+ return busy;
+ },
+
+ busyUntil: function (promise, operationDescription) {
+ // Freeze the UI until the promise is resolved. A timeout will unfreeze the
+ // UI, just in case the promise never gets resolved.
+ this._busyPromise = promise;
+ this._busyOperationDescription = operationDescription;
+ this.setupBusyTimeout();
+ this.busy();
+ promise.then(() => {
+ this.cancelBusyTimeout();
+ this.unbusy();
+ }, (e) => {
+ let message;
+ if (e && e.error && e.message) {
+ // Some errors come from fronts that are not based on protocol.js.
+ // Errors are not translated to strings.
+ message = operationDescription + " (" + e.error + "): " + e.message;
+ } else {
+ message = operationDescription + (e ? (": " + e) : "");
+ }
+ this.cancelBusyTimeout();
+ let operationCanceled = e && e.canceled;
+ if (!operationCanceled) {
+ UI.reportError("error_operationFail", message);
+ if (e) {
+ console.error(e);
+ }
+ }
+ this.unbusy();
+ });
+ return promise;
+ },
+
+ reportError: function (l10nProperty, ...l10nArgs) {
+ let text;
+
+ if (l10nArgs.length > 0) {
+ text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
+ } else {
+ text = Strings.GetStringFromName(l10nProperty);
+ }
+
+ console.error(text);
+
+ let buttons = [{
+ label: Strings.GetStringFromName("notification_showTroubleShooting_label"),
+ accessKey: Strings.GetStringFromName("notification_showTroubleShooting_accesskey"),
+ callback: function () {
+ Cmds.showTroubleShooting();
+ }
+ }];
+
+ let nbox = document.querySelector("#notificationbox");
+ nbox.removeAllNotifications(true);
+ nbox.appendNotification(text, "webide:errornotification", null,
+ nbox.PRIORITY_WARNING_LOW, buttons);
+ },
+
+ dismissErrorNotification: function () {
+ let nbox = document.querySelector("#notificationbox");
+ nbox.removeAllNotifications(true);
+ },
+
+ /** ******** COMMANDS **********/
+
+ /**
+ * This module emits various events when state changes occur.
+ *
+ * The events this module may emit include:
+ * busy:
+ * The window is currently busy and certain UI functions may be disabled.
+ * unbusy:
+ * The window is not busy and certain UI functions may be re-enabled.
+ */
+ update: function (what, details) {
+ this.emit("webide-update", what, details);
+ },
+
+ updateCommands: function () {
+ // Action commands
+ let playCmd = document.querySelector("#cmd_play");
+ let stopCmd = document.querySelector("#cmd_stop");
+ let debugCmd = document.querySelector("#cmd_toggleToolbox");
+ let playButton = document.querySelector("#action-button-play");
+ let projectPanelCmd = document.querySelector("#cmd_showProjectPanel");
+
+ if (document.querySelector("window").classList.contains("busy")) {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ projectPanelCmd.setAttribute("disabled", "true");
+ return;
+ }
+
+ if (!AppManager.selectedProject || !AppManager.connected) {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ } else {
+ let isProjectRunning = AppManager.isProjectRunning();
+ if (isProjectRunning) {
+ playButton.classList.add("reload");
+ stopCmd.removeAttribute("disabled");
+ debugCmd.removeAttribute("disabled");
+ } else {
+ playButton.classList.remove("reload");
+ stopCmd.setAttribute("disabled", "true");
+ debugCmd.setAttribute("disabled", "true");
+ }
+
+ // If connected and a project is selected
+ if (AppManager.selectedProject.type == "runtimeApp") {
+ playCmd.removeAttribute("disabled");
+ } else if (AppManager.selectedProject.type == "tab") {
+ playCmd.removeAttribute("disabled");
+ stopCmd.setAttribute("disabled", "true");
+ } else if (AppManager.selectedProject.type == "mainProcess") {
+ playCmd.setAttribute("disabled", "true");
+ stopCmd.setAttribute("disabled", "true");
+ } else {
+ if (AppManager.selectedProject.errorsCount == 0 &&
+ AppManager.runtimeCanHandleApps()) {
+ playCmd.removeAttribute("disabled");
+ } else {
+ playCmd.setAttribute("disabled", "true");
+ }
+ }
+ }
+
+ // Runtime commands
+ let monitorCmd = document.querySelector("#cmd_showMonitor");
+ let screenshotCmd = document.querySelector("#cmd_takeScreenshot");
+ let permissionsCmd = document.querySelector("#cmd_showPermissionsTable");
+ let detailsCmd = document.querySelector("#cmd_showRuntimeDetails");
+ let disconnectCmd = document.querySelector("#cmd_disconnectRuntime");
+ let devicePrefsCmd = document.querySelector("#cmd_showDevicePrefs");
+ let settingsCmd = document.querySelector("#cmd_showSettings");
+
+ if (AppManager.connected) {
+ if (AppManager.deviceFront) {
+ monitorCmd.removeAttribute("disabled");
+ detailsCmd.removeAttribute("disabled");
+ permissionsCmd.removeAttribute("disabled");
+ screenshotCmd.removeAttribute("disabled");
+ }
+ if (AppManager.preferenceFront) {
+ devicePrefsCmd.removeAttribute("disabled");
+ }
+ if (AppManager.settingsFront) {
+ settingsCmd.removeAttribute("disabled");
+ }
+ disconnectCmd.removeAttribute("disabled");
+ } else {
+ monitorCmd.setAttribute("disabled", "true");
+ detailsCmd.setAttribute("disabled", "true");
+ permissionsCmd.setAttribute("disabled", "true");
+ screenshotCmd.setAttribute("disabled", "true");
+ disconnectCmd.setAttribute("disabled", "true");
+ devicePrefsCmd.setAttribute("disabled", "true");
+ settingsCmd.setAttribute("disabled", "true");
+ }
+
+ let runtimePanelButton = document.querySelector("#runtime-panel-button");
+
+ if (AppManager.connected) {
+ runtimePanelButton.setAttribute("active", "true");
+ runtimePanelButton.removeAttribute("hidden");
+ } else {
+ runtimePanelButton.removeAttribute("active");
+ runtimePanelButton.setAttribute("hidden", "true");
+ }
+
+ projectPanelCmd.removeAttribute("disabled");
+ },
+
+ updateRemoveProjectButton: function () {
+ // Remove command
+ let removeCmdNode = document.querySelector("#cmd_removeProject");
+ if (AppManager.selectedProject) {
+ removeCmdNode.removeAttribute("disabled");
+ } else {
+ removeCmdNode.setAttribute("disabled", "true");
+ }
+ },
+
+ /** ******** RUNTIME **********/
+
+ get lastConnectedRuntime() {
+ return Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
+ },
+
+ set lastConnectedRuntime(runtime) {
+ Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime", runtime);
+ },
+
+ autoConnectRuntime: function () {
+ // Automatically reconnect to the previously selected runtime,
+ // if available and has an ID and feature is enabled
+ if (AppManager.selectedRuntime ||
+ !Services.prefs.getBoolPref("devtools.webide.autoConnectRuntime") ||
+ !this.lastConnectedRuntime) {
+ return;
+ }
+ let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
+
+ type = type.toLowerCase();
+
+ // Local connection is mapped to AppManager.runtimeList.other array
+ if (type == "local") {
+ type = "other";
+ }
+
+ // We support most runtimes except simulator, that needs to be manually
+ // launched
+ if (type == "usb" || type == "wifi" || type == "other") {
+ for (let runtime of AppManager.runtimeList[type]) {
+ // Some runtimes do not expose an id and don't support autoconnect (like
+ // remote connection)
+ if (runtime.id == id) {
+ // Only want one auto-connect attempt, so clear last runtime value
+ this.lastConnectedRuntime = "";
+ this.connectToRuntime(runtime);
+ }
+ }
+ }
+ },
+
+ connectToRuntime: function (runtime) {
+ let name = runtime.name;
+ let promise = AppManager.connectToRuntime(runtime);
+ promise.then(() => this.initConnectionTelemetry())
+ .catch(() => {
+ // Empty rejection handler to silence uncaught rejection warnings
+ // |busyUntil| will listen for rejections.
+ // Bug 1121100 may find a better way to silence these.
+ });
+ promise = this.busyUntil(promise, "Connecting to " + name);
+ // Stop busy timeout for runtimes that take unknown or long amounts of time
+ // to connect.
+ if (runtime.prolongedConnection) {
+ this.cancelBusyTimeout();
+ }
+ return promise;
+ },
+
+ updateRuntimeButton: function () {
+ let labelNode = document.querySelector("#runtime-panel-button > .panel-button-label");
+ if (!AppManager.selectedRuntime) {
+ labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label"));
+ } else {
+ let name = AppManager.selectedRuntime.name;
+ labelNode.setAttribute("value", name);
+ }
+ },
+
+ saveLastConnectedRuntime: function () {
+ if (AppManager.selectedRuntime &&
+ AppManager.selectedRuntime.id !== undefined) {
+ this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" +
+ AppManager.selectedRuntime.id;
+ } else {
+ this.lastConnectedRuntime = "";
+ }
+ },
+
+ /** ******** ACTIONS **********/
+
+ _actionsToLog: new Set(),
+
+ /**
+ * For each new connection, track whether play and debug were ever used. Only
+ * one value is collected for each button, even if they are used multiple
+ * times during a connection.
+ */
+ initConnectionTelemetry: function () {
+ this._actionsToLog.add("play");
+ this._actionsToLog.add("debug");
+ },
+
+ /**
+ * Action occurred. Log that it happened, and remove it from the loggable
+ * set.
+ */
+ onAction: function (action) {
+ if (!this._actionsToLog.has(action)) {
+ return;
+ }
+ this.logActionState(action, true);
+ this._actionsToLog.delete(action);
+ },
+
+ /**
+ * Connection status changed or we are shutting down. Record any loggable
+ * actions as having not occurred.
+ */
+ updateConnectionTelemetry: function () {
+ for (let action of this._actionsToLog.values()) {
+ this.logActionState(action, false);
+ }
+ this._actionsToLog.clear();
+ },
+
+ logActionState: function (action, state) {
+ let histogramId = "DEVTOOLS_WEBIDE_CONNECTION_" +
+ action.toUpperCase() + "_USED";
+ this._telemetry.log(histogramId, state);
+ },
+
+ /** ******** PROJECTS **********/
+
+ // ProjectEditor & details screen
+
+ destroyProjectEditor: function () {
+ if (this.projecteditor) {
+ this.projecteditor.destroy();
+ this.projecteditor = null;
+ }
+ },
+
+ /**
+ * Called when selecting or deselecting the project editor panel.
+ */
+ onChangeProjectEditorSelected: function () {
+ if (this.projecteditor) {
+ let panel = document.querySelector("#deck").selectedPanel;
+ if (panel && panel.id == "deck-panel-projecteditor") {
+ this.projecteditor.menuEnabled = true;
+ this._telemetry.toolOpened("webideProjectEditor");
+ } else {
+ this.projecteditor.menuEnabled = false;
+ this._telemetry.toolClosed("webideProjectEditor");
+ }
+ }
+ },
+
+ getProjectEditor: function () {
+ if (this.projecteditor) {
+ return this.projecteditor.loaded;
+ }
+
+ let projecteditorIframe = document.querySelector("#deck-panel-projecteditor");
+ this.projecteditor = ProjectEditor.ProjectEditor(projecteditorIframe, {
+ menubar: document.querySelector("#main-menubar"),
+ menuindex: 1
+ });
+ this.projecteditor.on("onEditorSave", () => {
+ AppManager.validateAndUpdateProject(AppManager.selectedProject);
+ this._telemetry.actionOccurred("webideProjectEditorSave");
+ });
+ return this.projecteditor.loaded;
+ },
+
+ updateProjectEditorHeader: function () {
+ let project = AppManager.selectedProject;
+ if (!project || !this.projecteditor) {
+ return;
+ }
+ let status = project.validationStatus || "unknown";
+ if (status == "error warning") {
+ status = "error";
+ }
+ this.getProjectEditor().then((projecteditor) => {
+ projecteditor.setProjectToAppPath(project.location, {
+ name: project.name,
+ iconUrl: project.icon,
+ projectOverviewURL: "chrome://webide/content/details.xhtml",
+ validationStatus: status
+ }).then(null, console.error);
+ }, console.error);
+ },
+
+ isProjectEditorEnabled: function () {
+ return Services.prefs.getBoolPref("devtools.webide.showProjectEditor");
+ },
+
+ openProject: function () {
+ let project = AppManager.selectedProject;
+
+ // Nothing to show
+
+ if (!project) {
+ this.resetDeck();
+ return;
+ }
+
+ // Make sure the directory exist before we show Project Editor
+
+ let forceDetailsOnly = false;
+ if (project.type == "packaged") {
+ forceDetailsOnly = !utils.doesFileExist(project.location);
+ }
+
+ // Show only the details screen
+
+ if (project.type != "packaged" ||
+ !this.isProjectEditorEnabled() ||
+ forceDetailsOnly) {
+ this.selectDeckPanel("details");
+ return;
+ }
+
+ // Show ProjectEditor
+
+ this.getProjectEditor().then(() => {
+ this.updateProjectEditorHeader();
+ }, console.error);
+
+ this.selectDeckPanel("projecteditor");
+ },
+
+ autoStartProject: Task.async(function* () {
+ let project = AppManager.selectedProject;
+
+ if (!project) {
+ return;
+ }
+ if (!(project.type == "runtimeApp" ||
+ project.type == "mainProcess" ||
+ project.type == "tab")) {
+ return; // For something that is not an editable app, we're done.
+ }
+
+ // Do not force opening apps that are already running, as they may have
+ // some activity being opened and don't want to dismiss them.
+ if (project.type == "runtimeApp" && !AppManager.isProjectRunning()) {
+ yield UI.busyUntil(AppManager.launchRuntimeApp(), "running app");
+ }
+ }),
+
+ autoOpenToolbox: Task.async(function* () {
+ let project = AppManager.selectedProject;
+
+ if (!project) {
+ return;
+ }
+ if (!(project.type == "runtimeApp" ||
+ project.type == "mainProcess" ||
+ project.type == "tab")) {
+ return; // For something that is not an editable app, we're done.
+ }
+
+ yield UI.createToolbox();
+ }),
+
+ importAndSelectApp: Task.async(function* (source) {
+ let isPackaged = !!source.path;
+ let project;
+ try {
+ project = yield AppProjects[isPackaged ? "addPackaged" : "addHosted"](source);
+ } catch (e) {
+ if (e === "Already added") {
+ // Select project that's already been added,
+ // and allow it to be revalidated and selected
+ project = AppProjects.get(isPackaged ? source.path : source);
+ } else {
+ throw e;
+ }
+ }
+
+ // Select project
+ AppManager.selectedProject = project;
+
+ this._telemetry.actionOccurred("webideImportProject");
+ }),
+
+ // Remember the last selected project on the runtime
+ saveLastSelectedProject: function () {
+ let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+ if (!shouldRestore) {
+ return;
+ }
+
+ // Ignore unselection of project on runtime disconnection
+ if (!AppManager.connected) {
+ return;
+ }
+
+ let project = "", type = "";
+ let selected = AppManager.selectedProject;
+ if (selected) {
+ if (selected.type == "runtimeApp") {
+ type = "runtimeApp";
+ project = selected.app.manifestURL;
+ } else if (selected.type == "mainProcess") {
+ type = "mainProcess";
+ } else if (selected.type == "packaged" ||
+ selected.type == "hosted") {
+ type = "local";
+ project = selected.location;
+ }
+ }
+ if (type) {
+ Services.prefs.setCharPref("devtools.webide.lastSelectedProject",
+ type + ":" + project);
+ } else {
+ Services.prefs.clearUserPref("devtools.webide.lastSelectedProject");
+ }
+ },
+
+ autoSelectProject: function () {
+ if (AppManager.selectedProject) {
+ return;
+ }
+ let shouldRestore = Services.prefs.getBoolPref("devtools.webide.restoreLastProject");
+ if (!shouldRestore) {
+ return;
+ }
+ let pref = Services.prefs.getCharPref("devtools.webide.lastSelectedProject");
+ if (!pref) {
+ return;
+ }
+ let m = pref.match(/^(\w+):(.*)$/);
+ if (!m) {
+ return;
+ }
+ let [_, type, project] = m;
+
+ if (type == "local") {
+ let lastProject = AppProjects.get(project);
+ if (lastProject) {
+ AppManager.selectedProject = lastProject;
+ }
+ }
+
+ // For other project types, we need to be connected to the runtime
+ if (!AppManager.connected) {
+ return;
+ }
+
+ if (type == "mainProcess" && AppManager.isMainProcessDebuggable()) {
+ AppManager.selectedProject = {
+ type: "mainProcess",
+ name: Strings.GetStringFromName("mainProcess_label"),
+ icon: AppManager.DEFAULT_PROJECT_ICON
+ };
+ } else if (type == "runtimeApp") {
+ let app = AppManager.apps.get(project);
+ if (app) {
+ AppManager.selectedProject = {
+ type: "runtimeApp",
+ app: app.manifest,
+ icon: app.iconURL,
+ name: app.manifest.name
+ };
+ }
+ }
+ },
+
+ /** ******** DECK **********/
+
+ setupDeck: function () {
+ let iframes = document.querySelectorAll("#deck > iframe");
+ for (let iframe of iframes) {
+ iframe.tooltip = "aHTMLTooltip";
+ }
+ },
+
+ resetFocus: function () {
+ document.commandDispatcher.focusedElement = document.documentElement;
+ },
+
+ selectDeckPanel: function (id) {
+ let deck = document.querySelector("#deck");
+ if (deck.selectedPanel && deck.selectedPanel.id === "deck-panel-" + id) {
+ // This panel is already displayed.
+ return;
+ }
+ this.resetFocus();
+ let panel = deck.querySelector("#deck-panel-" + id);
+ let lazysrc = panel.getAttribute("lazysrc");
+ if (lazysrc) {
+ panel.removeAttribute("lazysrc");
+ panel.setAttribute("src", lazysrc);
+ }
+ deck.selectedPanel = panel;
+ this.onChangeProjectEditorSelected();
+ },
+
+ resetDeck: function () {
+ this.resetFocus();
+ let deck = document.querySelector("#deck");
+ deck.selectedPanel = null;
+ this.onChangeProjectEditorSelected();
+ },
+
+ buildIDToDate(buildID) {
+ let fields = buildID.match(/(\d{4})(\d{2})(\d{2})/);
+ // Date expects 0 - 11 for months
+ return new Date(fields[1], Number.parseInt(fields[2]) - 1, fields[3]);
+ },
+
+ checkRuntimeVersion: Task.async(function* () {
+ if (AppManager.connected && AppManager.deviceFront) {
+ let desc = yield AppManager.deviceFront.getDescription();
+ // Compare device and firefox build IDs
+ // and only compare by day (strip hours/minutes) to prevent
+ // warning against builds of the same day.
+ let deviceID = desc.appbuildid.substr(0, 8);
+ let localID = Services.appinfo.appBuildID.substr(0, 8);
+ let deviceDate = this.buildIDToDate(deviceID);
+ let localDate = this.buildIDToDate(localID);
+ // Allow device to be newer by up to a week. This accommodates those with
+ // local device builds, since their devices will almost always be newer
+ // than the client.
+ if (deviceDate - localDate > 7 * MS_PER_DAY) {
+ this.reportError("error_runtimeVersionTooRecent", deviceID, localID);
+ }
+ }
+ }),
+
+ /** ******** TOOLBOX **********/
+
+ /**
+ * There are many ways to close a toolbox:
+ * * Close button inside the toolbox
+ * * Toggle toolbox wrench in WebIDE
+ * * Disconnect the current runtime gracefully
+ * * Yank cord out of device
+ * * Close or crash the app/tab
+ * We can't know for sure which one was used here, so reset the
+ * |toolboxPromise| since someone must be destroying it to reach here,
+ * and call our own close method.
+ */
+ _onToolboxClosed: function (promise, iframe) {
+ // Only save toolbox size, disable wrench button, workaround focus issue...
+ // if we are closing the last toolbox:
+ // - toolboxPromise is nullified by destroyToolbox and is still null here
+ // if no other toolbox has been opened in between,
+ // - having two distinct promise means we are receiving closed event
+ // for a previous, non-current, toolbox.
+ if (!this.toolboxPromise || this.toolboxPromise === promise) {
+ this.toolboxPromise = null;
+ this.resetFocus();
+ Services.prefs.setIntPref("devtools.toolbox.footer.height", iframe.height);
+
+ let splitter = document.querySelector(".devtools-horizontal-splitter");
+ splitter.setAttribute("hidden", "true");
+ document.querySelector("#action-button-debug").removeAttribute("active");
+ }
+ // We have to destroy the iframe, otherwise, the keybindings of webide don't work
+ // properly anymore.
+ iframe.remove();
+ },
+
+ destroyToolbox: function () {
+ // Only have a live toolbox if |this.toolboxPromise| exists
+ if (this.toolboxPromise) {
+ let toolboxPromise = this.toolboxPromise;
+ this.toolboxPromise = null;
+ return toolboxPromise.then(toolbox => toolbox.destroy());
+ }
+ return promise.resolve();
+ },
+
+ createToolbox: function () {
+ // If |this.toolboxPromise| exists, there is already a live toolbox
+ if (this.toolboxPromise) {
+ return this.toolboxPromise;
+ }
+
+ let iframe = document.createElement("iframe");
+ iframe.id = "toolbox";
+
+ // Compute a uid on the iframe in order to identify toolbox iframe
+ // when receiving toolbox-close event
+ iframe.uid = new Date().getTime();
+
+ let height = Services.prefs.getIntPref("devtools.toolbox.footer.height");
+ iframe.height = height;
+
+ let promise = this.toolboxPromise = AppManager.getTarget().then(target => {
+ return this._showToolbox(target, iframe);
+ }).then(toolbox => {
+ // Destroy the toolbox on WebIDE side before
+ // toolbox.destroy's promise resolves.
+ toolbox.once("destroyed", this._onToolboxClosed.bind(this, promise, iframe));
+ return toolbox;
+ }, console.error);
+
+ return this.busyUntil(this.toolboxPromise, "opening toolbox");
+ },
+
+ _showToolbox: function (target, iframe) {
+ let splitter = document.querySelector(".devtools-horizontal-splitter");
+ splitter.removeAttribute("hidden");
+
+ document.querySelector("notificationbox").insertBefore(iframe, splitter.nextSibling);
+ let host = Toolbox.HostType.CUSTOM;
+ let options = { customIframe: iframe, zoom: false, uid: iframe.uid };
+
+ document.querySelector("#action-button-debug").setAttribute("active", "true");
+
+ return gDevTools.showToolbox(target, null, host, options);
+ },
+
+ prePackageLog: function (msg) {
+ if (msg == "start") {
+ UI.selectDeckPanel("logs");
+ }
+ }
+};
+
+EventEmitter.decorate(UI);
+
+var Cmds = {
+ quit: function () {
+ if (UI.canCloseProject()) {
+ window.close();
+ }
+ },
+
+ showProjectPanel: function () {
+ ProjectPanel.toggleSidebar();
+ return promise.resolve();
+ },
+
+ showRuntimePanel: function () {
+ RuntimeScanners.scan();
+ RuntimePanel.toggleSidebar();
+ },
+
+ disconnectRuntime: function () {
+ let disconnecting = Task.spawn(function* () {
+ yield UI.destroyToolbox();
+ yield AppManager.disconnectRuntime();
+ });
+ return UI.busyUntil(disconnecting, "disconnecting from runtime");
+ },
+
+ takeScreenshot: function () {
+ let url = AppManager.deviceFront.screenshotToDataURL();
+ return UI.busyUntil(url.then(longstr => {
+ return longstr.string().then(dataURL => {
+ longstr.release().then(null, console.error);
+ UI.openInBrowser(dataURL);
+ });
+ }), "taking screenshot");
+ },
+
+ showPermissionsTable: function () {
+ UI.selectDeckPanel("permissionstable");
+ },
+
+ showRuntimeDetails: function () {
+ UI.selectDeckPanel("runtimedetails");
+ },
+
+ showDevicePrefs: function () {
+ UI.selectDeckPanel("devicepreferences");
+ },
+
+ showSettings: function () {
+ UI.selectDeckPanel("devicesettings");
+ },
+
+ showMonitor: function () {
+ UI.selectDeckPanel("monitor");
+ },
+
+ play: Task.async(function* () {
+ let busy;
+ switch (AppManager.selectedProject.type) {
+ case "packaged":
+ let autosave =
+ Services.prefs.getBoolPref("devtools.webide.autosaveFiles");
+ if (autosave && UI.projecteditor) {
+ yield UI.projecteditor.saveAllFiles();
+ }
+ busy = UI.busyWithProgressUntil(AppManager.installAndRunProject(),
+ "installing and running app");
+ break;
+ case "hosted":
+ busy = UI.busyUntil(AppManager.installAndRunProject(),
+ "installing and running app");
+ break;
+ case "runtimeApp":
+ busy = UI.busyUntil(AppManager.launchOrReloadRuntimeApp(), "launching / reloading app");
+ break;
+ case "tab":
+ busy = UI.busyUntil(AppManager.reloadTab(), "reloading tab");
+ break;
+ }
+ if (!busy) {
+ return promise.reject();
+ }
+ UI.onAction("play");
+ return busy;
+ }),
+
+ stop: function () {
+ return UI.busyUntil(AppManager.stopRunningApp(), "stopping app");
+ },
+
+ toggleToolbox: function () {
+ UI.onAction("debug");
+ if (UI.toolboxPromise) {
+ UI.destroyToolbox();
+ return promise.resolve();
+ } else {
+ return UI.createToolbox();
+ }
+ },
+
+ removeProject: function () {
+ AppManager.removeSelectedProject();
+ },
+
+ toggleEditors: function () {
+ let isNowEnabled = !UI.isProjectEditorEnabled();
+ Services.prefs.setBoolPref("devtools.webide.showProjectEditor", isNowEnabled);
+ if (!isNowEnabled) {
+ UI.destroyProjectEditor();
+ }
+ UI.openProject();
+ },
+
+ showTroubleShooting: function () {
+ UI.openInBrowser(HELP_URL);
+ },
+
+ showAddons: function () {
+ UI.selectDeckPanel("addons");
+ },
+
+ showPrefs: function () {
+ UI.selectDeckPanel("prefs");
+ },
+
+ zoomIn: function () {
+ if (UI.contentViewer.fullZoom < MAX_ZOOM) {
+ UI.contentViewer.fullZoom += 0.1;
+ Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
+ }
+ },
+
+ zoomOut: function () {
+ if (UI.contentViewer.fullZoom > MIN_ZOOM) {
+ UI.contentViewer.fullZoom -= 0.1;
+ Services.prefs.setCharPref("devtools.webide.zoom", UI.contentViewer.fullZoom);
+ }
+ },
+
+ resetZoom: function () {
+ UI.contentViewer.fullZoom = 1;
+ Services.prefs.setCharPref("devtools.webide.zoom", 1);
+ }
+};