Mypal/devtools/client/webide/content/webide.js

1158 lines
36 KiB
JavaScript

/* 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);
}
};