From a33bf3b9237d56a7d97f821162d8491220cc42c8 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Mon, 12 May 2014 08:53:00 +0200 Subject: [PATCH] Bug 999417 - Land the new App Manager UI. r=jryans r=mshal --- browser/app/profile/firefox.js | 7 + browser/devtools/app-manager/app-projects.js | 56 +- browser/devtools/app-manager/app-validator.js | 2 + browser/devtools/framework/gDevTools.jsm | 12 +- browser/devtools/moz.build | 3 + browser/devtools/webide/Makefile.in | 10 + browser/devtools/webide/content/details.js | 122 +++ browser/devtools/webide/content/details.xhtml | 52 ++ browser/devtools/webide/content/jar.mn | 12 + browser/devtools/webide/content/moz.build | 7 + browser/devtools/webide/content/newapp.js | 161 ++++ browser/devtools/webide/content/newapp.xul | 33 + browser/devtools/webide/content/webide.js | 730 ++++++++++++++++++ browser/devtools/webide/content/webide.xul | 164 ++++ browser/devtools/webide/locales/Makefile.in | 5 + .../devtools/webide/locales/en-US/webide.dtd | 80 ++ .../webide/locales/en-US/webide.properties | 13 + browser/devtools/webide/locales/jar.mn | 10 + browser/devtools/webide/locales/moz.build | 7 + .../devtools/webide/modules/app-manager.js | 540 +++++++++++++ browser/devtools/webide/moz.build | 12 + browser/devtools/webide/themes/details.css | 135 ++++ browser/devtools/webide/themes/icons.png | Bin 0 -> 28072 bytes browser/devtools/webide/themes/jar.mn | 11 + browser/devtools/webide/themes/moz.build | 7 + browser/devtools/webide/themes/newapp.css | 58 ++ browser/devtools/webide/themes/throbber.svg | 22 + browser/devtools/webide/themes/webide.css | 279 +++++++ browser/devtools/webide/webide-prefs.js | 6 + configure.in | 13 + 30 files changed, 2560 insertions(+), 9 deletions(-) create mode 100644 browser/devtools/webide/Makefile.in create mode 100644 browser/devtools/webide/content/details.js create mode 100644 browser/devtools/webide/content/details.xhtml create mode 100644 browser/devtools/webide/content/jar.mn create mode 100644 browser/devtools/webide/content/moz.build create mode 100644 browser/devtools/webide/content/newapp.js create mode 100644 browser/devtools/webide/content/newapp.xul create mode 100644 browser/devtools/webide/content/webide.js create mode 100644 browser/devtools/webide/content/webide.xul create mode 100644 browser/devtools/webide/locales/Makefile.in create mode 100644 browser/devtools/webide/locales/en-US/webide.dtd create mode 100644 browser/devtools/webide/locales/en-US/webide.properties create mode 100644 browser/devtools/webide/locales/jar.mn create mode 100644 browser/devtools/webide/locales/moz.build create mode 100644 browser/devtools/webide/modules/app-manager.js create mode 100644 browser/devtools/webide/moz.build create mode 100644 browser/devtools/webide/themes/details.css create mode 100644 browser/devtools/webide/themes/icons.png create mode 100644 browser/devtools/webide/themes/jar.mn create mode 100644 browser/devtools/webide/themes/moz.build create mode 100644 browser/devtools/webide/themes/newapp.css create mode 100644 browser/devtools/webide/themes/throbber.svg create mode 100644 browser/devtools/webide/themes/webide.css create mode 100644 browser/devtools/webide/webide-prefs.js diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 048a42c02ffe9..447e5eb9beca5 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1239,6 +1239,13 @@ pref("devtools.appmanager.enabled", true); pref("devtools.appmanager.lastTab", "help"); pref("devtools.appmanager.manifestEditor.enabled", true); +// Enable devtools webide +#ifdef MOZ_DEVTOOLS_WEBIDE +pref("devtools.webide.enabled", true); +#else +pref("devtools.webide.enabled", false); +#endif + // Toolbox preferences pref("devtools.toolbox.footer.height", 250); pref("devtools.toolbox.sidebar.width", 500); diff --git a/browser/devtools/app-manager/app-projects.js b/browser/devtools/app-manager/app-projects.js index d09f72f706efc..23c1a3cadbfc6 100644 --- a/browser/devtools/app-manager/app-projects.js +++ b/browser/devtools/app-manager/app-projects.js @@ -1,9 +1,10 @@ -const {Cc,Ci,Cu} = require("chrome"); +const {Cc,Ci,Cu,Cr} = require("chrome"); const ObservableObject = require("devtools/shared/observable-object"); const promise = require("devtools/toolkit/deprecated-sync-thenables"); const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); const {generateUUID} = Cc['@mozilla.org/uuid-generator;1'].getService(Ci.nsIUUIDGenerator); +const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm"); /** * IndexedDB wrapper that just save project objects @@ -37,20 +38,45 @@ const IDB = { let db = IDB._db = request.result; let objectStore = db.transaction("projects").objectStore("projects"); let projects = [] + let toRemove = []; objectStore.openCursor().onsuccess = function(event) { let cursor = event.target.result; if (cursor) { if (cursor.value.location) { + // We need to make sure this object has a `.location` property. // The UI depends on this property. // This should not be needed as we make sure to register valid // projects, but in the past (before bug 924568), we might have // registered invalid objects. - projects.push(cursor.value); + + + // We also want to make sure the location is valid. + // If the location doesn't exist, we remove the project. + + try { + let file = FileUtils.File(cursor.value.location); + if (file.exists()) { + projects.push(cursor.value); + } else { + toRemove.push(cursor.value.location); + } + } catch (e) { + if (e.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH) { + // A URL + projects.push(cursor.value); + } + } } cursor.continue(); } else { - deferred.resolve(projects); + let removePromises = []; + for (let location of toRemove) { + removePromises.push(IDB.remove(location)); + } + promise.all(removePromises).then(() => { + deferred.resolve(projects); + }); } }; }; @@ -83,6 +109,12 @@ const IDB = { update: function(project) { let deferred = promise.defer(); + // Clone object to make it storable by IndexedDB. + // Projects are proxified objects (for the template + // mechanismn in the first version of the App Manager). + // This will change in the future. + project = JSON.parse(JSON.stringify(project)); + var transaction = IDB._db.transaction(["projects"], "readwrite"); var objectStore = transaction.objectStore("projects"); var request = objectStore.put(project); @@ -131,6 +163,14 @@ const AppProjects = { }, addPackaged: function(folder) { + let file = FileUtils.File(folder.path); + if (!file.exists()) { + return promise.reject("path doesn't exist"); + } + let existingProject = this.get(folder.path); + if (existingProject) { + return promise.reject("Already added"); + } let project = { type: "packaged", location: folder.path, @@ -151,6 +191,10 @@ const AppProjects = { }, addHosted: function(manifestURL) { + let existingProject = this.get(manifestURL); + if (existingProject) { + return promise.reject("Already added"); + } let project = { type: "hosted", location: manifestURL @@ -163,11 +207,7 @@ const AppProjects = { }, update: function (project) { - return IDB.update({ - type: project.type, - location: project.location, - packagedAppOrigin: project.packagedAppOrigin - }).then(() => project); + return IDB.update(project); }, remove: function(location) { diff --git a/browser/devtools/app-manager/app-validator.js b/browser/devtools/app-manager/app-validator.js index f0120cf56fc29..df4da3e5450ab 100644 --- a/browser/devtools/app-manager/app-validator.js +++ b/browser/devtools/app-manager/app-validator.js @@ -56,6 +56,7 @@ AppValidator.prototype._fetchManifest = function (manifestURL) { this.manifestURL = manifestURL; let req = new XMLHttpRequest(); + req.overrideMimeType('text/plain'); try { req.open("GET", manifestURL, true); } catch(e) { @@ -155,6 +156,7 @@ AppValidator.prototype.validateLaunchPath = function (manifest) { } let req = new XMLHttpRequest(); + req.overrideMimeType('text/plain'); try { req.open("HEAD", indexURL, true); } catch(e) { diff --git a/browser/devtools/framework/gDevTools.jsm b/browser/devtools/framework/gDevTools.jsm index c662b7df91222..58afdbede0c97 100644 --- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -522,7 +522,17 @@ let gDevToolsBrowser = { * Open the App Manager */ openAppManager: function(gBrowser) { - gBrowser.selectedTab = gBrowser.addTab("about:app-manager"); + if (Services.prefs.getBoolPref("devtools.webide.enabled")) { + let win = Services.wm.getMostRecentWindow("devtools:webide"); + if (win) { + win.focus(); + } else { + let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher); + ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null); + } + } else { + gBrowser.selectedTab = gBrowser.addTab("about:app-manager"); + } }, /** diff --git a/browser/devtools/moz.build b/browser/devtools/moz.build index f35eb89f2e407..fc0898d4e06ac 100644 --- a/browser/devtools/moz.build +++ b/browser/devtools/moz.build @@ -29,6 +29,9 @@ DIRS += [ 'webconsole', ] +if CONFIG['MOZ_DEVTOOLS_WEBIDE']: + DIRS += ['webide'] + EXTRA_COMPONENTS += [ 'devtools-clhandler.js', 'devtools-clhandler.manifest', diff --git a/browser/devtools/webide/Makefile.in b/browser/devtools/webide/Makefile.in new file mode 100644 index 0000000000000..2384b0621ed4c --- /dev/null +++ b/browser/devtools/webide/Makefile.in @@ -0,0 +1,10 @@ +# 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/. + +PREF_JS_EXPORTS = $(srcdir)/webide-prefs.js + +include $(topsrcdir)/config/rules.mk + +libs:: + $(NSINSTALL) $(srcdir)/modules/*.js $(FINAL_TARGET)/modules/devtools diff --git a/browser/devtools/webide/content/details.js b/browser/devtools/webide/content/details.js new file mode 100644 index 0000000000000..9fff912b9d28e --- /dev/null +++ b/browser/devtools/webide/content/details.js @@ -0,0 +1,122 @@ +/* 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 = Components.utils; +Cu.import("resource:///modules/devtools/gDevTools.jsm"); +const {Services} = Cu.import("resource://gre/modules/Services.jsm"); +const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; +const {AppProjects} = require("devtools/app-manager/app-projects"); +const {AppValidator} = require("devtools/app-manager/app-validator"); +const {AppManager} = require("devtools/app-manager"); + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + document.addEventListener("visibilitychange", updateUI, true); + AppManager.on("app-manager-update", onAppManagerUpdate); + updateUI(); +}, true); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + AppManager.off("app-manager-update", onAppManagerUpdate); +}, true); + +function onAppManagerUpdate(event, what, details) { + if (what == "project" || + what == "project-validated") { + updateUI(); + } +} + +function resetUI() { + document.querySelector("#toolbar").classList.add("hidden"); + document.querySelector("#type").classList.add("hidden"); + document.querySelector("#descriptionHeader").classList.add("hidden"); + document.querySelector("#manifestURLHeader").classList.add("hidden"); + document.querySelector("#locationHeader").classList.add("hidden"); + + document.body.className = ""; + document.querySelector("#icon").src = ""; + document.querySelector("h1").textContent = ""; + document.querySelector("#description").textContent = ""; + document.querySelector("#type").textContent = ""; + document.querySelector("#manifestURL").textContent = ""; + document.querySelector("#location").textContent = ""; + + document.querySelector("#errorslist").innerHTML = ""; + document.querySelector("#warningslist").innerHTML = ""; + +} + +function updateUI() { + resetUI(); + + let project = AppManager.selectedProject; + if (!project) { + return; + } + + if (project.type != "runtimeApp") { + document.querySelector("#toolbar").classList.remove("hidden"); + document.querySelector("#locationHeader").classList.remove("hidden"); + document.querySelector("#location").textContent = project.location; + } + + document.body.className = project.validationStatus; + document.querySelector("#icon").src = project.icon; + document.querySelector("h1").textContent = project.name; + + let manifest; + if (project.type == "runtimeApp") { + manifest = project.app.manifest; + } else { + manifest = project.manifest; + } + + if (manifest) { + if (manifest.description) { + document.querySelector("#descriptionHeader").classList.remove("hidden"); + document.querySelector("#description").textContent = manifest.description; + } + + document.querySelector("#type").classList.remove("hidden"); + + if (project.type == "runtimeApp") { + document.querySelector("#type").textContent = manifest.type || "web"; + } else { + document.querySelector("#type").textContent = project.type + " " + (manifest.type || "web"); + } + + if (project.type == "packaged") { + let manifest = AppManager.getProjectManifestURL(project); + if (manifest) { + document.querySelector("#manifestURLHeader").classList.remove("hidden"); + document.querySelector("#manifestURL").textContent = manifest; + } + } + } + + let errorsNode = document.querySelector("#errorslist"); + let warningsNode = document.querySelector("#warningslist"); + + if (project.errors) { + for (let e of project.errors) { + let li = document.createElement("li"); + li.textContent = e; + errorsNode.appendChild(li); + } + } + + if (project.warnings) { + for (let w of project.warnings) { + let li = document.createElement("li"); + li.textContent = w; + warningsNode.appendChild(li); + } + } +} + +function removeProject() { + AppManager.removeSelectedProject(); +} diff --git a/browser/devtools/webide/content/details.xhtml b/browser/devtools/webide/content/details.xhtml new file mode 100644 index 0000000000000..0396e4dc0a36a --- /dev/null +++ b/browser/devtools/webide/content/details.xhtml @@ -0,0 +1,52 @@ + + + + + + %webideDTD; +]> + + + + + + + + + +
+ +

+ &details_valid_header; + &details_warning_header; + &details_error_header; +

+
+ +
+ +
+

+

+
+
+ +
+

&details_description;

+

+ +

&details_location;

+

+ +

&details_manifestURL;

+

+
+ + + + + + diff --git a/browser/devtools/webide/content/jar.mn b/browser/devtools/webide/content/jar.mn new file mode 100644 index 0000000000000..53c7d5b252549 --- /dev/null +++ b/browser/devtools/webide/content/jar.mn @@ -0,0 +1,12 @@ +# 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/. + +webide.jar: +% content webide %content/ + content/webide.xul (webide.xul) + content/webide.js (webide.js) + content/newapp.xul (newapp.xul) + content/newapp.js (newapp.js) + content/details.xhtml (details.xhtml) + content/details.js (details.js) diff --git a/browser/devtools/webide/content/moz.build b/browser/devtools/webide/content/moz.build new file mode 100644 index 0000000000000..3bbe6729759cd --- /dev/null +++ b/browser/devtools/webide/content/moz.build @@ -0,0 +1,7 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +JAR_MANIFESTS += ['jar.mn'] diff --git a/browser/devtools/webide/content/newapp.js b/browser/devtools/webide/content/newapp.js new file mode 100644 index 0000000000000..a52209e3399b4 --- /dev/null +++ b/browser/devtools/webide/content/newapp.js @@ -0,0 +1,161 @@ +/* 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 Cc = Components.classes; +const Cu = Components.utils; +const Ci = Components.interfaces; +Cu.import("resource://gre/modules/Services.jsm"); + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "ZipUtils", "resource://gre/modules/ZipUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Downloads.jsm"); + +const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools; +const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm"); +const {AppProjects} = require("devtools/app-manager/app-projects"); +const APP_CREATOR_LIST = "devtools.webide.templatesURL"; +const {AppManager} = require("devtools/app-manager"); + +let gTemplateList = null; + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + let projectNameNode = document.querySelector("#project-name"); + projectNameNode.addEventListener("input", canValidate, true); + getJSON(); +}, true); + +function getJSON() { + let xhr = new XMLHttpRequest(); + xhr.overrideMimeType('text/plain'); + xhr.onload = function() { + let list; + try { + list = JSON.parse(this.responseText); + if (!Array.isArray(list)) { + throw new Error("JSON response not an array"); + } + if (list.length == 0) { + throw new Error("JSON response is an empty array"); + } + } catch(e) { + return failAndBail("Invalid response from server"); + } + gTemplateList = list; + let templatelistNode = document.querySelector("#templatelist"); + templatelistNode.innerHTML = ""; + for (let template of list) { + let richlistitemNode = document.createElement("richlistitem"); + let imageNode = document.createElement("image"); + imageNode.setAttribute("src", template.icon); + let labelNode = document.createElement("label"); + labelNode.setAttribute("value", template.name); + let descriptionNode = document.createElement("description"); + descriptionNode.textContent = template.description; + let vboxNode = document.createElement("vbox"); + vboxNode.setAttribute("flex", "1"); + richlistitemNode.appendChild(imageNode); + vboxNode.appendChild(labelNode); + vboxNode.appendChild(descriptionNode); + richlistitemNode.appendChild(vboxNode); + templatelistNode.appendChild(richlistitemNode); + } + templatelistNode.selectedIndex = 0; + }; + xhr.onerror = function() { + failAndBail("Can't download app templates"); + }; + let url = Services.prefs.getCharPref(APP_CREATOR_LIST); + xhr.open("get", url); + xhr.send(); +} + +function failAndBail(msg) { + let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService); + promptService.alert(window, "error", msg); + window.close(); +} + +function canValidate() { + let projectNameNode = document.querySelector("#project-name"); + let dialogNode = document.querySelector("dialog"); + if (projectNameNode.value.length > 0) { + dialogNode.removeAttribute("buttondisabledaccept"); + } else { + dialogNode.setAttribute("buttondisabledaccept", "true"); + } +} + +function doOK() { + let projectName = document.querySelector("#project-name").value; + + if (!projectName) { + AppManager.console.error("No project name"); + return false; + } + + if (!gTemplateList) { + AppManager.console.error("No template index"); + return false; + } + + let templatelistNode = document.querySelector("#templatelist"); + if (templatelistNode.selectedIndex < 0) { + AppManager.console.error("No template selected"); + return false; + } + + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, "Select directory where to create app directory", Ci.nsIFilePicker.modeGetFolder); + let res = fp.show(); + if (res == Ci.nsIFilePicker.returnCancel) { + AppManager.console.error("No directory selected"); + return false; + } + let folder = fp.file; + + // Create subfolder with fs-friendly name of project + let subfolder = projectName.replace(/\W/g, '').toLowerCase(); + folder.append(subfolder); + + try { + folder.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY); + } catch(e) { + AppManager.console.error(e); + return false; + } + + // Download boilerplate zip + let template = gTemplateList[templatelistNode.selectedIndex]; + let source = template.file; + let target = folder.clone(); + target.append(subfolder + ".zip"); + + let bail = (e) => { + AppManager.console.error(e); + window.close(); + }; + + Downloads.fetch(source, target).then(() => { + ZipUtils.extractFiles(target, folder); + target.remove(false); + AppProjects.addPackaged(folder).then((project) => { + window.arguments[0].location = project.location; + AppManager.validateProject(project).then(() => { + if (project.manifest) { + project.manifest.name = projectName; + AppManager.writeManifest(project).then(() => { + AppManager.validateProject(project).then( + () => {window.close()}, bail) + }, bail) + } else { + bail("Manifest not found"); + } + }, bail) + }, bail) + }, bail); + + return false; +} diff --git a/browser/devtools/webide/content/newapp.xul b/browser/devtools/webide/content/newapp.xul new file mode 100644 index 0000000000000..8749a4e695007 --- /dev/null +++ b/browser/devtools/webide/content/newapp.xul @@ -0,0 +1,33 @@ + + + + + + %webideDTD; +]> + + + + + + + + diff --git a/browser/devtools/webide/content/webide.js b/browser/devtools/webide/content/webide.js new file mode 100644 index 0000000000000..194fbb45993c6 --- /dev/null +++ b/browser/devtools/webide/content/webide.js @@ -0,0 +1,730 @@ +/* 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 Cc = Components.classes; +const Cu = Components.utils; +const Ci = Components.interfaces; + +Cu.import("resource:///modules/devtools/gDevTools.jsm"); + +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); +const {require} = devtools; +const {Services} = Cu.import("resource://gre/modules/Services.jsm"); +const {AppProjects} = require("devtools/app-manager/app-projects"); +const {Connection} = require("devtools/client/connection-manager"); +const {AppManager} = require("devtools/app-manager"); + +const Strings = Services.strings.createBundle("chrome://webide/locale/webide.properties"); + +const HTML = "http://www.w3.org/1999/xhtml"; + +window.addEventListener("load", function onLoad() { + window.removeEventListener("load", onLoad); + UI.init(); +}); + +window.addEventListener("unload", function onUnload() { + window.removeEventListener("unload", onUnload); + UI.uninit(); +}); + +let UI = { + init: function() { + AppManager.init(); + + this.onMessage = this.onMessage.bind(this); + window.addEventListener("message", this.onMessage); + + this.appManagerUpdate = this.appManagerUpdate.bind(this); + AppManager.on("app-manager-update", this.appManagerUpdate); + + this.logNode = document.querySelector("#logs"); + + this.updateCommands(); + this.updateRuntimeList(); + + this.onfocus = this.onfocus.bind(this); + window.addEventListener("focus", this.onfocus, true); + + try { + let lastProjectLocation = Services.prefs.getCharPref("devtools.webide.lastprojectlocation"); + AppProjects.load().then(() => { + let lastProject = AppProjects.get(lastProjectLocation); + if (lastProject) { + AppManager.selectedProject = lastProject; + } else { + AppManager.selectedProject = null; + } + }); + } catch(e) { + AppManager.selectedProject = null; + } + + document.querySelector("#toggle-logs").addEventListener("click", function() { + document.querySelector("#logs").classList.toggle("expand"); + UI.logNode.scrollTop = UI.logNode.scrollTopMax; + }); + }, + + uninit: function() { + window.removeEventListener("focus", this.onfocus, true); + AppManager.off("app-manager-update", this.appManagerUpdate); + AppManager.uninit(); + window.removeEventListener("message", this.onMessage); + }, + + 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 != "runtimeApp") { + AppManager.validateProject(AppManager.selectedProject); + } + }, + + appManagerUpdate: function(event, what, details) { + // Got a message from app-manager.js + switch (what) { + case "console": + if (details.level == "log") this.console.log(details.message); + if (details.level == "warning") this.console.warning(details.message); + if (details.level == "error") this.console.error(details.message); + if (details.level == "success") this.console.success(details.message); + break; + case "runtimelist": + this.updateRuntimeList(); + break; + case "connection": + this.updateRuntimeButton(); + this.updateCommands(); + break; + case "project": + this.updateTitle(); + this.closeToolbox(); + this.updateCommands(); + this.updateProjectButton(); + this.openProject(); + break; + case "project-is-not-running": + case "project-is-running": + this.updateCommands(); + break; + case "runtime": + this.updateRuntimeButton(); + break; + case "project-validated": + this.updateTitle(); + this.updateCommands(); + this.updateProjectButton(); + break; + }; + }, + + openInBrowser: function(url) { + // Open a URL in a Firefox window + let browserWin = Services.wm.getMostRecentWindow("navigator:browser"); + if (browserWin) { + let gBrowser = browserWin.gBrowser; + gBrowser.selectedTab = gBrowser.addTab(url); + browserWin.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"); + } + }, + + hidePanels: function() { + let panels = document.querySelectorAll("panel"); + for (let p of panels) { + p.hidePopup(); + } + }, + + busy: function() { + document.querySelector("window").classList.add("busy") + this.updateCommands(); + }, + + unbusy: function() { + document.querySelector("window").classList.remove("busy") + this.updateCommands(); + }, + + busyUntil: function(promise, operationDescription) { + // Freeze the UI until the promise is resolved. A 30s timeout + // will unfreeze the UI, just in case the promise never gets + // resolved. + let timeout = setTimeout(() => { + this.unbusy(); + this.console.error("Operation timeout: " + operationDescription); + }, 30000); + this.busy(); + promise.then(() => { + clearTimeout(timeout); + this.unbusy(); + }, () => { + clearTimeout(timeout); + this.unbusy(); + }); + }, + + /********** RUNTIME **********/ + + updateRuntimeList: function() { + let USBListNode = document.querySelector("#runtime-panel-usbruntime"); + let simulatorListNode = document.querySelector("#runtime-panel-simulators"); + while (USBListNode.hasChildNodes()) { + USBListNode.firstChild.remove(); + } + while (simulatorListNode.hasChildNodes()) { + simulatorListNode.firstChild.remove(); + } + + this.console.log("Found " + AppManager.runtimeList.usb.length + " USB devices."); + this.console.log("Found " + AppManager.runtimeList.simulators.length + " simulators."); + for (let runtime of AppManager.runtimeList.usb) { + let panelItemNode = document.createElement("toolbarbutton"); + panelItemNode.className = "panel-item runtime-panel-item-usbruntime"; + panelItemNode.setAttribute("label", runtime.getName()); + USBListNode.appendChild(panelItemNode); + let r = runtime; + panelItemNode.addEventListener("click", () => { + this.hidePanels(); + this.connectToRuntime(r); + }, true); + } + + for (let runtime of AppManager.runtimeList.simulators) { + let panelItemNode = document.createElement("toolbarbutton"); + panelItemNode.className = "panel-item runtime-panel-item-simulator"; + panelItemNode.setAttribute("label", runtime.getName()); + simulatorListNode.appendChild(panelItemNode); + let r = runtime; + panelItemNode.addEventListener("click", () => { + this.hidePanels(); + this.connectToRuntime(r); + }, true); + } + + }, + + connectToRuntime: function(runtime) { + let name = runtime.getName(); + let promise = AppManager.connectToRuntime(runtime); + this.busyUntil(promise, "connecting to runtime"); + promise.then( + () => {this.console.success("Connected to " + name)}, + () => {this.console.error("Can't connect to " + name)}); + }, + + updateRuntimeButton: function() { + let buttonNode = document.querySelector("#runtime-panel-button"); + let labelNode = buttonNode.querySelector(".panel-button-label"); + if (!AppManager.selectedRuntime) { + labelNode.setAttribute("value", Strings.GetStringFromName("runtimeButton_label")); + } else { + let name = AppManager.selectedRuntime.getName(); + labelNode.setAttribute("value", name); + } + }, + + /********** PROJECTS **********/ + + // Panel & button + + updateProjectButton: function() { + let buttonNode = document.querySelector("#project-panel-button"); + let labelNode = buttonNode.querySelector(".panel-button-label"); + let imageNode = buttonNode.querySelector(".panel-button-image"); + + let project = AppManager.selectedProject; + + if (!project) { + buttonNode.classList.add("no-project"); + labelNode.setAttribute("value", Strings.GetStringFromName("projectButton_label")); + imageNode.removeAttribute("src"); + } else { + buttonNode.classList.remove("no-project"); + labelNode.setAttribute("value", project.name); + imageNode.setAttribute("src", project.icon); + } + }, + + // details.xhtml + + openProject: function() { + let details = document.querySelector("#details"); + let project = AppManager.selectedProject; + + if (!project) { + details.setAttribute("hidden", "true"); + return; + } + + if (project.location) { + Services.prefs.setCharPref("devtools.webide.lastprojectlocation", project.location); + } + + details.removeAttribute("hidden"); + }, + + /********** COMMANDS **********/ + + updateCommands: function() { + + if (document.querySelector("window").classList.contains("busy")) { + document.querySelector("#cmd_newApp").setAttribute("disabled", "true"); + document.querySelector("#cmd_importPackagedApp").setAttribute("disabled", "true"); + document.querySelector("#cmd_importHostedApp").setAttribute("disabled", "true"); + document.querySelector("#cmd_showProjectPanel").setAttribute("disabled", "true"); + document.querySelector("#cmd_showRuntimePanel").setAttribute("disabled", "true"); + document.querySelector("#cmd_removeProject").setAttribute("disabled", "true"); + document.querySelector("#cmd_disconnectRuntime").setAttribute("disabled", "true"); + document.querySelector("#cmd_showPermissionsTable").setAttribute("disabled", "true"); + document.querySelector("#cmd_takeScreenshot").setAttribute("disabled", "true"); + document.querySelector("#cmd_showRuntimeDetails").setAttribute("disabled", "true"); + document.querySelector("#cmd_play").setAttribute("disabled", "true"); + document.querySelector("#cmd_stop").setAttribute("disabled", "true"); + document.querySelector("#cmd_toggleToolbox").setAttribute("disabled", "true"); + return; + } + + document.querySelector("#cmd_newApp").removeAttribute("disabled"); + document.querySelector("#cmd_importPackagedApp").removeAttribute("disabled"); + document.querySelector("#cmd_importHostedApp").removeAttribute("disabled"); + document.querySelector("#cmd_showProjectPanel").removeAttribute("disabled"); + document.querySelector("#cmd_showRuntimePanel").removeAttribute("disabled"); + + document.querySelector("#runtime-panel-button").removeAttribute("active"); + + // Action commands + let playCmd = document.querySelector("#cmd_play"); + let stopCmd = document.querySelector("#cmd_stop"); + let debugCmd = document.querySelector("#cmd_toggleToolbox"); + + if (!AppManager.selectedProject || AppManager.connection.status != Connection.Status.CONNECTED) { + playCmd.setAttribute("disabled", "true"); + stopCmd.setAttribute("disabled", "true"); + debugCmd.setAttribute("disabled", "true"); + } else { + let isProjectRunning = AppManager.isProjectRunning(); + if (isProjectRunning) { + stopCmd.removeAttribute("disabled"); + debugCmd.removeAttribute("disabled"); + } else { + stopCmd.setAttribute("disabled", "true"); + debugCmd.setAttribute("disabled", "true"); + } + + // If connected and a project is selected + if (AppManager.selectedProject.type == "runtimeApp") { + if (isProjectRunning) { + playCmd.setAttribute("disabled", "true"); + } else { + playCmd.removeAttribute("disabled"); + } + } else { + if (AppManager.selectedProject.errorsCount == 0) { + playCmd.removeAttribute("disabled"); + } else { + playCmd.setAttribute("disabled", "true"); + } + } + document.querySelector("#runtime-panel-button").setAttribute("active", "true"); + } + + // Remove command + let removeCmdNode = document.querySelector("#cmd_removeProject"); + if (AppManager.selectedProject) { + removeCmdNode.removeAttribute("disabled"); + } else { + removeCmdNode.setAttribute("disabled", "true"); + } + + // Runtime commands + 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 box = document.querySelector("#runtime-actions"); + + if (AppManager.connection.status == Connection.Status.CONNECTED) { + screenshotCmd.removeAttribute("disabled"); + permissionsCmd.removeAttribute("disabled"); + disconnectCmd.removeAttribute("disabled"); + detailsCmd.removeAttribute("disabled"); + box.removeAttribute("hidden"); + } else { + screenshotCmd.setAttribute("disabled", "true"); + permissionsCmd.setAttribute("disabled", "true"); + disconnectCmd.setAttribute("disabled", "true"); + detailsCmd.setAttribute("disabled", "true"); + box.setAttribute("hidden", "true"); + } + + }, + + /********** TOOLBOX **********/ + + onMessage: function(event) { + // The custom toolbox sends a message to its parent + // window. + try { + let json = JSON.parse(event.data); + switch (json.name) { + case "toolbox-close": + this.closeToolboxUI(); + break; + } + } catch(e) { Cu.reportError(e); } + }, + + closeToolbox: function() { + if (this.toolboxPromise) { + this.toolboxPromise.then(toolbox => { + toolbox.destroy(); + document.querySelector("#action-button-debug").removeAttribute("active"); + this.toolboxPromise = null; + }, this.console.error); + } + }, + + showToolbox: function(target) { + if (this.toolboxIframe) { + return; + } + + let splitter = document.querySelector(".devtools-horizontal-splitter"); + splitter.removeAttribute("hidden"); + + let iframe = document.createElement("iframe"); + document.querySelector("window").insertBefore(iframe, splitter.nextSibling); + let host = devtools.Toolbox.HostType.CUSTOM; + let options = { customIframe: iframe }; + this.toolboxIframe = iframe; + + let height = Services.prefs.getIntPref("devtools.toolbox.footer.height"); + iframe.height = height; + + document.querySelector("#action-button-debug").setAttribute("active", "true"); + + return gDevTools.showToolbox(target, null, host, options); + }, + + closeToolboxUI: function() { + let body = document.querySelector("#body"); + body.removeAttribute("hidden"); + + Services.prefs.setIntPref("devtools.toolbox.footer.height", this.toolboxIframe.height); + + // We have to destroy the iframe, otherwise, the keybindings of webide don't work + // properly anymore. + this.toolboxIframe.remove(); + this.toolboxIframe = null; + + let splitter = document.querySelector(".devtools-horizontal-splitter"); + splitter.setAttribute("hidden", "true"); + }, + + console: { + _log: function(msg, classname) { + let li = document.createElementNS(HTML, "p"); + li.textContent = msg; + li.className = classname; + UI.logNode.appendChild(li); + UI.logNode.scrollTop = UI.logNode.scrollTopMax; + }, + log: function(msg) { + UI.console._log(msg, "log"); + console.log(msg); + }, + warning: function(msg) { + UI.console._log(msg, "warning"); + console.warning(msg); + }, + error: function(msg) { + UI.console._log(msg, "error"); + console.error(msg); + }, + success: function(msg) { + UI.console._log(msg, "success"); + console.log(msg); + }, + }, +} + + +let Cmds = { + quit: function() { + window.close(); + }, + + newApp: function() { + UI.hidePanels(); + let ret = {location:null}; + window.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret); + if (!ret.location) + return; + let project = AppProjects.get(ret.location); + UI.busyUntil(AppManager.validateProject(project).then(() => { + UI.console.success("New project created at " + ret.location); + AppManager.selectedProject = project; + }, (e) => UI.console.error("Error while create new app: " + e)), "creating new app");; + }, + + importPackagedApp: function() { + UI.hidePanels(); + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); + fp.init(window, Strings.GetStringFromName("importPackagedApp_title"), Ci.nsIFilePicker.modeGetFolder); + let res = fp.show(); + if (res == Ci.nsIFilePicker.returnCancel) + return; + UI.busyUntil(AppProjects.addPackaged(fp.file) + .then(project => AppManager.validateProject(project)) + .then(project => AppManager.selectedProject = project) + .then(( ) => { UI.console.log("New project successfuly added") }, + (e) => { UI.console.error("Error while importing project: " + e) }), + "importing packaged app"); + }, + + + importHostedApp: function() { + UI.hidePanels(); + let promptService = Cc["@mozilla.org/embedcomp/prompt-service;1"].getService(Ci.nsIPromptService); + let ret = {value:null}; + promptService.prompt(window, + Strings.GetStringFromName("importHostedApp_title"), + Strings.GetStringFromName("importHostedApp_header"), + ret, null, {}); + let url = ret.value; + if (!url) + return; + UI.busyUntil(AppProjects.addHosted(url) + .then(project => AppManager.validateProject(project)) + .then(project => AppManager.selectedProject = project) + .then(( ) => { UI.console.log("New project successfuly added") }, + (e) => { UI.console.error("Error while importing project: " + e) }), + "importing hosted app"); + }, + + + showProjectPanel: function() { + let panelNode = document.querySelector("#project-panel"); + let panelVboxNode = document.querySelector("#project-panel > vbox"); + let anchorNode = document.querySelector("#project-panel-button > .panel-button-anchor"); + let projectsNode = document.querySelector("#project-panel-projects"); + + while (projectsNode.hasChildNodes()) { + projectsNode.firstChild.remove(); + } + + panelNode.openPopup(anchorNode); + panelVboxNode.scrollTop = 0; + + AppProjects.load().then(() => { + let projects = AppProjects.store.object.projects; + for (let i = 0; i < projects.length; i++) { + let project = projects[i]; + let panelItemNode = document.createElement("toolbarbutton"); + panelItemNode.className = "panel-item"; + projectsNode.appendChild(panelItemNode); + panelItemNode.setAttribute("label", project.name || AppManager.DEFAULT_PROJECT_NAME); + panelItemNode.setAttribute("image", project.icon || AppManager.DEFAULT_PROJECT_ICON); + if (!project.validationStatus) { + // The result of the validation process (storing names, icons, …) has never been + // stored in the IndexedDB database. This happens when the project has been created + // from the old app manager. We need to run the validation again and update the name + // and icon of the app + AppManager.validateProject(project).then(() => { + panelItemNode.setAttribute("label", project.name); + panelItemNode.setAttribute("image", project.icon); + }); + } + panelItemNode.addEventListener("click", () => { + UI.hidePanels(); + AppManager.selectedProject = project; + }, true); + } + }, UI.console.error); + + + let runtimeappsHeaderNode = document.querySelector("#panel-header-runtimeapps"); + if (AppManager.connection.status == Connection.Status.CONNECTED) { + runtimeappsHeaderNode.removeAttribute("hidden"); + } else { + runtimeappsHeaderNode.setAttribute("hidden", "true"); + } + + let runtimeAppsNode = document.querySelector("#project-panel-runtimeapps"); + while (runtimeAppsNode.hasChildNodes()) { + runtimeAppsNode.firstChild.remove(); + } + + UI.console.log("Found " + AppManager.webAppsStore.object.all.length + " apps"); + + for (let i = 0; i < AppManager.webAppsStore.object.all.length; i++) { + let app = AppManager.webAppsStore.object.all[i]; + let panelItemNode = document.createElement("toolbarbutton"); + panelItemNode.className = "panel-item"; + panelItemNode.setAttribute("label", app.name); + panelItemNode.setAttribute("image", app.iconURL); + runtimeAppsNode.appendChild(panelItemNode); + panelItemNode.addEventListener("click", () => { + UI.hidePanels(); + AppManager.selectedProject = { + type: "runtimeApp", + app: app, + icon: app.iconURL, + name: app.name + }; + }, true); + } + }, + + showRuntimePanel: function() { + let panel = document.querySelector("#runtime-panel"); + let anchor = document.querySelector("#runtime-panel-button > .panel-button-anchor"); + panel.openPopup(anchor); + }, + + disconnectRuntime: function() { + UI.busyUntil(AppManager.disconnectRuntime()); + }, + + takeScreenshot: function() { + UI.hidePanels(); + UI.busyUntil(AppManager.deviceFront.screenshotToDataURL().then(longstr => { + return longstr.string().then(dataURL => { + longstr.release().then(null, UI.console.error); + UI.openInBrowser(dataURL); + }); + })); + }, + + showPermissionsTable: function() { + UI.hidePanels(); + UI.busyUntil(AppManager.deviceFront.getRawPermissionsTable().then(json => { + let styleContent = ""; + styleContent += "body {background:white; font-family: monospace}"; + styleContent += "table {border-collapse: collapse}"; + styleContent += "th, td {padding: 5px; border: 1px solid #EEE}"; + styleContent += "th {min-width: 130px}"; + styleContent += "td {text-align: center}"; + styleContent += "th:first-of-type, td:first-of-type {text-align:left}"; + styleContent += ".permallow {color:rgb(152, 207, 57)}"; + styleContent += ".permprompt {color:rgb(0,158,237)}"; + styleContent += ".permdeny {color:rgb(204,73,8)}"; + let style = document.createElementNS(HTML, "style"); + style.textContent = styleContent; + let table = document.createElementNS(HTML, "table"); + table.innerHTML = "Nametype:webtype:privilegedtype:certified"; + let permissionsTable = json.rawPermissionsTable; + for (let name in permissionsTable) { + let tr = document.createElementNS(HTML, "tr"); + let td = document.createElementNS(HTML, "td"); + td.textContent = name; + tr.appendChild(td); + for (let type of ["app","privileged","certified"]) { + let td = document.createElementNS(HTML, "td"); + if (permissionsTable[name][type] == json.ALLOW_ACTION) { + td.textContent = "✓"; + td.className = "permallow"; + } + if (permissionsTable[name][type] == json.PROMPT_ACTION) { + td.textContent = "!"; + td.className = "permprompt"; + } + if (permissionsTable[name][type] == json.DENY_ACTION) { + td.textContent = "✕"; + td.className = "permdeny" + } + tr.appendChild(td); + } + table.appendChild(tr); + } + let body = document.createElementNS(HTML, "body"); + body.appendChild(style); + body.appendChild(table); + let url = "data:text/html;charset=utf-8,"; + url += encodeURIComponent(body.outerHTML); + UI.openInBrowser(url); + }), "showing permission table"); + }, + + showRuntimeDetails: function() { + UI.hidePanels(); + UI.busyUntil(AppManager.deviceFront.getDescription().then(json => { + let styleContent = ""; + styleContent += "body {background:white; font-family: monospace}"; + styleContent += "table {border-collapse: collapse}"; + styleContent += "th, td {padding: 5px; border: 1px solid #EEE}"; + let style = document.createElementNS(HTML, "style"); + style.textContent = styleContent; + let table = document.createElementNS(HTML, "table"); + for (let name in json) { + let tr = document.createElementNS(HTML, "tr"); + let td = document.createElementNS(HTML, "td"); + td.textContent = name; + tr.appendChild(td); + td = document.createElementNS(HTML, "td"); + td.textContent = json[name]; + tr.appendChild(td); + table.appendChild(tr); + } + let body = document.createElementNS(HTML, "body"); + body.appendChild(style); + body.appendChild(table); + let url = "data:text/html;charset=utf-8,"; + url += encodeURIComponent(body.outerHTML); + UI.openInBrowser(url); + }), "showing runtime details"); + + }, + + play: function() { + switch(AppManager.selectedProject.type) { + case "packaged": + case "hosted": + UI.busyUntil(AppManager.installAndRunProject(), "installing and running app"); + break; + case "runtimeApp": + UI.busyUntil(AppManager.runRuntimeApp(), "running app"); + break; + } + }, + + stop: function() { + UI.busyUntil(AppManager.stopRunningApp(), "stopping app"); + }, + + toggleToolbox: function() { + if (UI.toolboxIframe) { + UI.closeToolbox(); + } else { + UI.toolboxPromise = AppManager.getTarget().then((target) => { + return UI.showToolbox(target); + }, UI.console.error); + UI.busyUntil(UI.toolboxPromise, "opening toolbox"); + } + }, + + removeProject: function() { + AppManager.removeSelectedProject(); + }, + + toggleEditors: function() { + // Toggle Itchpad + }, +} diff --git a/browser/devtools/webide/content/webide.xul b/browser/devtools/webide/content/webide.xul new file mode 100644 index 0000000000000..99efc5295eefb --- /dev/null +++ b/browser/devtools/webide/content/webide.xul @@ -0,0 +1,164 @@ + + + + + + %webideDTD; +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +