summaryrefslogtreecommitdiffstats
path: root/devtools/client/webide/modules/project-list.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/webide/modules/project-list.js')
-rw-r--r--devtools/client/webide/modules/project-list.js375
1 files changed, 375 insertions, 0 deletions
diff --git a/devtools/client/webide/modules/project-list.js b/devtools/client/webide/modules/project-list.js
new file mode 100644
index 000000000..10766dd4f
--- /dev/null
+++ b/devtools/client/webide/modules/project-list.js
@@ -0,0 +1,375 @@
+/* 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;
+ }
+};