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