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