diff options
Diffstat (limited to 'devtools/client/webide/content/webide.js')
-rw-r--r-- | devtools/client/webide/content/webide.js | 1157 |
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); + } +}; |