Mypal/devtools/client/webide/modules/project-list.js

376 lines
12 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/. */
const {Cu} = require("chrome");
const Services = require("Services");
const {AppProjects} = require("devtools/client/webide/modules/app-projects");
const {AppManager} = require("devtools/client/webide/modules/app-manager");
const promise = require("promise");
const EventEmitter = require("devtools/shared/event-emitter");
const {Task} = require("devtools/shared/task");
const utils = require("devtools/client/webide/modules/utils");
const Telemetry = require("devtools/client/shared/telemetry");
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
var ProjectList;
module.exports = ProjectList = function (win, parentWindow) {
EventEmitter.decorate(this);
this._doc = win.document;
this._UI = parentWindow.UI;
this._parentWindow = parentWindow;
this._telemetry = new Telemetry();
this._panelNodeEl = "div";
this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
this._UI.on("webide-update", this.onWebIDEUpdate);
AppManager.init();
this.appManagerUpdate = this.appManagerUpdate.bind(this);
AppManager.on("app-manager-update", this.appManagerUpdate);
};
ProjectList.prototype = {
get doc() {
return this._doc;
},
appManagerUpdate: function (event, what, details) {
// Got a message from app-manager.js
// See AppManager.update() for descriptions of what these events mean.
switch (what) {
case "project-removed":
case "runtime-apps-icons":
case "runtime-targets":
case "connection":
this.update(details);
break;
case "project":
this.updateCommands();
this.update(details);
break;
}
},
onWebIDEUpdate: function (event, what, details) {
if (what == "busy" || what == "unbusy") {
this.updateCommands();
}
},
/**
* testOptions: { chrome mochitest support
* folder: nsIFile, where to store the app
* index: Number, index of the app in the template list
* name: String name of the app
* }
*/
newApp: function (testOptions) {
let parentWindow = this._parentWindow;
let self = this;
return this._UI.busyUntil(Task.spawn(function* () {
// Open newapp.xul, which will feed ret.location
let ret = {location: null, testOptions: testOptions};
parentWindow.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret);
if (!ret.location)
return;
// Retrieve added project
let project = AppProjects.get(ret.location);
// Select project
AppManager.selectedProject = project;
self._telemetry.actionOccurred("webideNewProject");
}), "creating new app");
},
importPackagedApp: function (location) {
let parentWindow = this._parentWindow;
let UI = this._UI;
return UI.busyUntil(Task.spawn(function* () {
let directory = utils.getPackagedDirectory(parentWindow, location);
if (!directory) {
// User cancelled directory selection
return;
}
yield UI.importAndSelectApp(directory);
}), "importing packaged app");
},
importHostedApp: function (location) {
let parentWindow = this._parentWindow;
let UI = this._UI;
return UI.busyUntil(Task.spawn(function* () {
let url = utils.getHostedURL(parentWindow, location);
if (!url) {
return;
}
yield UI.importAndSelectApp(url);
}), "importing hosted app");
},
/**
* opts: {
* panel: Object, currenl project panel node
* name: String, name of the project
* icon: String path of the project icon
* }
*/
_renderProjectItem: function (opts) {
let span = opts.panel.querySelector("span") || this._doc.createElement("span");
span.textContent = opts.name;
let icon = opts.panel.querySelector("img") || this._doc.createElement("img");
icon.className = "project-image";
icon.setAttribute("src", opts.icon);
opts.panel.appendChild(icon);
opts.panel.appendChild(span);
opts.panel.setAttribute("title", opts.name);
},
refreshTabs: function () {
if (AppManager.connected) {
return AppManager.listTabs().then(() => {
this.updateTabs();
}).catch(console.error);
}
},
updateTabs: function () {
let tabsHeaderNode = this._doc.querySelector("#panel-header-tabs");
let tabsNode = this._doc.querySelector("#project-panel-tabs");
while (tabsNode.hasChildNodes()) {
tabsNode.firstChild.remove();
}
if (!AppManager.connected) {
tabsHeaderNode.setAttribute("hidden", "true");
return;
}
let tabs = AppManager.tabStore.tabs;
tabsHeaderNode.removeAttribute("hidden");
for (let i = 0; i < tabs.length; i++) {
let tab = tabs[i];
let URL = this._parentWindow.URL;
let url;
try {
url = new URL(tab.url);
} catch (e) {
// Don't try to handle invalid URLs, especially from Valence.
continue;
}
// Wanted to use nsIFaviconService here, but it only works for visited
// tabs, so that's no help for any remote tabs. Maybe some favicon wizard
// knows how to get high-res favicons easily, or we could offer actor
// support for this (bug 1061654).
if (url.origin) {
tab.favicon = url.origin + "/favicon.ico";
}
tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
if (url.protocol.startsWith("http")) {
tab.name = url.hostname + ": " + tab.name;
}
let panelItemNode = this._doc.createElement(this._panelNodeEl);
panelItemNode.className = "panel-item";
tabsNode.appendChild(panelItemNode);
this._renderProjectItem({
panel: panelItemNode,
name: tab.name,
icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON
});
panelItemNode.addEventListener("click", () => {
AppManager.selectedProject = {
type: "tab",
app: tab,
icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON,
location: tab.url,
name: tab.name
};
}, true);
}
return promise.resolve();
},
updateApps: function () {
let doc = this._doc;
let runtimeappsHeaderNode = doc.querySelector("#panel-header-runtimeapps");
let sortedApps = [];
for (let [manifestURL, app] of AppManager.apps) {
sortedApps.push(app);
}
sortedApps = sortedApps.sort((a, b) => {
return a.manifest.name > b.manifest.name;
});
let mainProcess = AppManager.isMainProcessDebuggable();
if (AppManager.connected && (sortedApps.length > 0 || mainProcess)) {
runtimeappsHeaderNode.removeAttribute("hidden");
} else {
runtimeappsHeaderNode.setAttribute("hidden", "true");
}
let runtimeAppsNode = doc.querySelector("#project-panel-runtimeapps");
while (runtimeAppsNode.hasChildNodes()) {
runtimeAppsNode.firstChild.remove();
}
if (mainProcess) {
let panelItemNode = doc.createElement(this._panelNodeEl);
panelItemNode.className = "panel-item";
this._renderProjectItem({
panel: panelItemNode,
name: Strings.GetStringFromName("mainProcess_label"),
icon: AppManager.DEFAULT_PROJECT_ICON
});
runtimeAppsNode.appendChild(panelItemNode);
panelItemNode.addEventListener("click", () => {
AppManager.selectedProject = {
type: "mainProcess",
name: Strings.GetStringFromName("mainProcess_label"),
icon: AppManager.DEFAULT_PROJECT_ICON
};
}, true);
}
for (let i = 0; i < sortedApps.length; i++) {
let app = sortedApps[i];
let panelItemNode = doc.createElement(this._panelNodeEl);
panelItemNode.className = "panel-item";
this._renderProjectItem({
panel: panelItemNode,
name: app.manifest.name,
icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON
});
runtimeAppsNode.appendChild(panelItemNode);
panelItemNode.addEventListener("click", () => {
AppManager.selectedProject = {
type: "runtimeApp",
app: app.manifest,
icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON,
name: app.manifest.name
};
}, true);
}
return promise.resolve();
},
updateCommands: function () {
let doc = this._doc;
let newAppCmd;
let packagedAppCmd;
let hostedAppCmd;
newAppCmd = doc.querySelector("#new-app");
packagedAppCmd = doc.querySelector("#packaged-app");
hostedAppCmd = doc.querySelector("#hosted-app");
if (!newAppCmd || !packagedAppCmd || !hostedAppCmd) {
return;
}
if (this._parentWindow.document.querySelector("window").classList.contains("busy")) {
newAppCmd.setAttribute("disabled", "true");
packagedAppCmd.setAttribute("disabled", "true");
hostedAppCmd.setAttribute("disabled", "true");
return;
}
newAppCmd.removeAttribute("disabled");
packagedAppCmd.removeAttribute("disabled");
hostedAppCmd.removeAttribute("disabled");
},
/**
* Trigger an update of the project and remote runtime list.
* @param options object (optional)
* An |options| object containing a type of |apps| or |tabs| will limit
* what is updated to only those sections.
*/
update: function (options) {
let deferred = promise.defer();
if (options && options.type === "apps") {
return this.updateApps();
} else if (options && options.type === "tabs") {
return this.updateTabs();
}
let doc = this._doc;
let projectsNode = doc.querySelector("#project-panel-projects");
while (projectsNode.hasChildNodes()) {
projectsNode.firstChild.remove();
}
AppProjects.load().then(() => {
let projects = AppProjects.projects;
for (let i = 0; i < projects.length; i++) {
let project = projects[i];
let panelItemNode = doc.createElement(this._panelNodeEl);
panelItemNode.className = "panel-item";
projectsNode.appendChild(panelItemNode);
if (!project.validationStatus) {
// The result of the validation process (storing names, icons, …) is not stored in
// the IndexedDB database when App Manager v1 is used.
// We need to run the validation again and update the name and icon of the app.
AppManager.validateAndUpdateProject(project).then(() => {
this._renderProjectItem({
panel: panelItemNode,
name: project.name,
icon: project.icon
});
});
} else {
this._renderProjectItem({
panel: panelItemNode,
name: project.name || AppManager.DEFAULT_PROJECT_NAME,
icon: project.icon || AppManager.DEFAULT_PROJECT_ICON
});
}
panelItemNode.addEventListener("click", () => {
AppManager.selectedProject = project;
}, true);
}
deferred.resolve();
}, deferred.reject);
// List remote apps and the main process, if they exist
this.updateApps();
// Build the tab list right now, so it's fast...
this.updateTabs();
// But re-list them and rebuild, in case any tabs navigated since the last
// time they were listed.
if (AppManager.connected) {
AppManager.listTabs().then(() => {
this.updateTabs();
}).catch(console.error);
}
return deferred.promise;
},
destroy: function () {
this._doc = null;
AppManager.off("app-manager-update", this.appManagerUpdate);
this._UI.off("webide-update", this.onWebIDEUpdate);
this._UI = null;
this._parentWindow = null;
this._panelNodeEl = null;
}
};