diff --git a/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js b/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js index 1f626aec6d99..2954d4cb24be 100644 --- a/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js +++ b/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js @@ -7,9 +7,9 @@ const promise = require("projecteditor/helpers/promise"); const ProjectEditor = require("projecteditor/projecteditor"); const SAMPLE_PATH = buildTempDirectoryStructure(); -const SAMPLE_NAME = "DevTools Content"; +const SAMPLE_NAME = "DevTools Content Application Name"; const SAMPLE_PROJECT_URL = "http://mozilla.org"; -const SAMPLE_ICON = "chrome://browser/skin/devtools/tool-options.svg"; +const SAMPLE_ICON = "chrome://browser/skin/devtools/tool-debugger.svg"; /** * Create a workspace for working on projecteditor, available at @@ -56,11 +56,13 @@ document.addEventListener("DOMContentLoaded", function onDOMReady(e) { projecteditor.setProjectToAppPath(SAMPLE_PATH, { name: SAMPLE_NAME, iconUrl: SAMPLE_ICON, - projectOverviewURL: SAMPLE_PROJECT_URL + projectOverviewURL: SAMPLE_PROJECT_URL, + validationStatus: "valid" }).then(() => { let allResources = projecteditor.project.allResources(); console.log("All resources have been loaded", allResources, allResources.map(r=>r.basename).join("|")); }); + }); }, false); diff --git a/browser/devtools/projecteditor/chrome/content/projecteditor.xul b/browser/devtools/projecteditor/chrome/content/projecteditor.xul index 9f5e59d49357..0ab13a74d4f2 100644 --- a/browser/devtools/projecteditor/chrome/content/projecteditor.xul +++ b/browser/devtools/projecteditor/chrome/content/projecteditor.xul @@ -4,10 +4,8 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - - diff --git a/browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js b/browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js index fc79ee3960e3..d3979236de74 100644 --- a/browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js +++ b/browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js @@ -5,6 +5,7 @@ const { emit } = require("sdk/event/core"); const promise = require("projecteditor/helpers/promise"); var { registerPlugin, Plugin } = require("projecteditor/plugins/core"); const { AppProjectEditor } = require("./app-project-editor"); +const OPTION_URL = "chrome://browser/skin/devtools/tool-options.svg"; var AppManagerRenderer = Class({ extends: Plugin, @@ -25,20 +26,32 @@ var AppManagerRenderer = Class({ let {appManagerOpts} = this.host.project; let doc = elt.ownerDocument; let image = doc.createElement("image"); - let label = doc.createElement("label"); + let optionImage = doc.createElement("image"); + let flexElement = doc.createElement("div"); + let nameLabel = doc.createElement("span"); + let statusElement = doc.createElement("div"); - label.className = "project-name-label"; image.className = "project-image"; + optionImage.className = "project-options"; + nameLabel.className = "project-name-label"; + statusElement.className = "project-status"; + flexElement.className = "project-flex"; let name = appManagerOpts.name || resource.basename; let url = appManagerOpts.iconUrl || "icon-sample.png"; + let status = appManagerOpts.validationStatus || "unknown"; - label.textContent = name; + nameLabel.textContent = name; image.setAttribute("src", url); + optionImage.setAttribute("src", OPTION_URL); + statusElement.setAttribute("status", status) elt.innerHTML = ""; elt.appendChild(image); - elt.appendChild(label); + elt.appendChild(nameLabel); + elt.appendChild(flexElement); + elt.appendChild(statusElement); + elt.appendChild(optionImage); return true; } }); diff --git a/browser/devtools/projecteditor/lib/project.js b/browser/devtools/projecteditor/lib/project.js index 8b19117ae24d..59d0dc513a63 100644 --- a/browser/devtools/projecteditor/lib/project.js +++ b/browser/devtools/projecteditor/lib/project.js @@ -136,13 +136,11 @@ var Project = Class({ /** * Get every file path used inside of the project. * - * @returns generator-iterator + * @returns Array * A list of all file paths */ - allPaths: function*() { - for (let [path, store] of this.localStores) { - yield path; - } + allPaths: function() { + return [path for (path of this.localStores.keys())]; }, /** diff --git a/browser/devtools/projecteditor/lib/projecteditor.js b/browser/devtools/projecteditor/lib/projecteditor.js index 2e3ca9653f86..d0e0efd5742b 100644 --- a/browser/devtools/projecteditor/lib/projecteditor.js +++ b/browser/devtools/projecteditor/lib/projecteditor.js @@ -264,14 +264,29 @@ var ProjectEditor = Class({ * @param string path * The file path to set * @param Object opts - * Custom options used by the project. See plugins/app-manager. + * Custom options used by the project. + * - name: display name for project + * - iconUrl: path to icon for project + * - validationStatus: one of 'unknown|error|warning|valid' + * - projectOverviewURL: path to load for iframe when project + * is selected in the tree. * @param Promise * Promise that is resolved once the project is ready to be used. */ setProjectToAppPath: function(path, opts = {}) { this.project.appManagerOpts = opts; - this.project.removeAllStores(); - this.project.addPath(path); + + let existingPaths = this.project.allPaths(); + if (existingPaths.length !== 1 || existingPaths[0] !== path) { + // Only fully reset if this is a new path. + this.project.removeAllStores(); + this.project.addPath(path); + } else { + // Otherwise, just ask for the root to be redrawn + let rootResource = this.project.localStores.get(path).root; + emit(rootResource, "label-change", rootResource); + } + return this.project.refresh(); }, diff --git a/browser/devtools/projecteditor/lib/tree.js b/browser/devtools/projecteditor/lib/tree.js index 7edeeadd545e..efe2b1a88663 100644 --- a/browser/devtools/projecteditor/lib/tree.js +++ b/browser/devtools/projecteditor/lib/tree.js @@ -39,7 +39,7 @@ var ResourceContainer = Class({ this.line = doc.createElementNS(HTML_NS, "div"); this.line.classList.add("child"); - this.line.classList.add("side-menu-widget-item"); + this.line.classList.add("entry"); this.line.setAttribute("theme", "dark"); this.line.setAttribute("tabindex", "0"); @@ -223,15 +223,14 @@ var TreeView = Class({ this.models = new Set(); this.roots = new Set(); this._containers = new Map(); - this.elt = document.createElement("vbox"); + this.elt = document.createElementNS(HTML_NS, "div"); this.elt.tree = this; - this.elt.className = "side-menu-widget-container sources-tree"; + this.elt.className = "sources-tree"; this.elt.setAttribute("with-arrows", "true"); this.elt.setAttribute("theme", "dark"); this.elt.setAttribute("flex", "1"); this.children = document.createElementNS(HTML_NS, "ul"); - this.children.setAttribute("flex", "1"); this.elt.appendChild(this.children); this.resourceChildrenChanged = this.resourceChildrenChanged.bind(this); @@ -315,7 +314,7 @@ var TreeView = Class({ return; } let container = this.importResource(root); - container.line.classList.add("side-menu-widget-group-title"); + container.line.classList.add("entry-group-title"); container.line.setAttribute("theme", "dark"); this.selectContainer(container); diff --git a/browser/devtools/projecteditor/test/browser.ini b/browser/devtools/projecteditor/test/browser.ini index 965ff56493e3..a39efe1e5e4e 100644 --- a/browser/devtools/projecteditor/test/browser.ini +++ b/browser/devtools/projecteditor/test/browser.ini @@ -5,6 +5,7 @@ support-files = head.js helper_homepage.html +[browser_projecteditor_app_options.js] [browser_projecteditor_delete_file.js] [browser_projecteditor_editing_01.js] [browser_projecteditor_immediate_destroy.js] diff --git a/browser/devtools/projecteditor/test/browser_projecteditor_app_options.js b/browser/devtools/projecteditor/test/browser_projecteditor_app_options.js new file mode 100644 index 000000000000..2141c19b033b --- /dev/null +++ b/browser/devtools/projecteditor/test/browser_projecteditor_app_options.js @@ -0,0 +1,102 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test that options can be changed without resetting the whole +// editor. +let test = asyncTest(function*() { + + let TEMP_PATH = buildTempDirectoryStructure(); + let projecteditor = yield addProjectEditorTab(); + + let resourceBeenAdded = promise.defer(); + projecteditor.project.once("resource-added", () => { + info ("A resource has been added"); + resourceBeenAdded.resolve(); + }); + + info ("About to set project to: " + TEMP_PATH); + yield projecteditor.setProjectToAppPath(TEMP_PATH, { + name: "Test", + iconUrl: "chrome://browser/skin/devtools/tool-options.svg", + projectOverviewURL: SAMPLE_WEBAPP_URL + }); + + info ("Making sure a resource has been added before continuing"); + yield resourceBeenAdded.promise; + + info ("From now on, if a resource is added it should fail"); + projecteditor.project.on("resource-added", failIfResourceAdded); + + info ("Getting ahold and validating the project header DOM"); + let header = projecteditor.document.querySelector(".entry-group-title"); + let image = header.querySelector(".project-image"); + let nameLabel = header.querySelector(".project-name-label"); + let statusElement = header.querySelector(".project-status"); + is (statusElement.getAttribute("status"), "unknown", "The status starts out as unknown."); + is (nameLabel.textContent, "Test", "The name label has been set correctly"); + is (image.getAttribute("src"), "chrome://browser/skin/devtools/tool-options.svg", "The icon has been set correctly"); + + info ("About to set project with new options."); + yield projecteditor.setProjectToAppPath(TEMP_PATH, { + name: "Test2", + iconUrl: "chrome://browser/skin/devtools/tool-inspector.svg", + projectOverviewURL: SAMPLE_WEBAPP_URL, + validationStatus: "error" + }); + + ok (!nameLabel.parentNode, "The old elements have been removed"); + + info ("Getting ahold of and validating the project header DOM"); + let image = header.querySelector(".project-image"); + let nameLabel = header.querySelector(".project-name-label"); + let statusElement = header.querySelector(".project-status"); + is (statusElement.getAttribute("status"), "error", "The status has been set correctly."); + is (nameLabel.textContent, "Test2", "The name label has been set correctly"); + is (image.getAttribute("src"), "chrome://browser/skin/devtools/tool-inspector.svg", "The icon has been set correctly"); + + info ("About to set project with new options."); + yield projecteditor.setProjectToAppPath(TEMP_PATH, { + name: "Test3", + iconUrl: "chrome://browser/skin/devtools/tool-webconsole.svg", + projectOverviewURL: SAMPLE_WEBAPP_URL, + validationStatus: "warning" + }); + + ok (!nameLabel.parentNode, "The old elements have been removed"); + + info ("Getting ahold of and validating the project header DOM"); + let image = header.querySelector(".project-image"); + let nameLabel = header.querySelector(".project-name-label"); + let statusElement = header.querySelector(".project-status"); + is (statusElement.getAttribute("status"), "warning", "The status has been set correctly."); + is (nameLabel.textContent, "Test3", "The name label has been set correctly"); + is (image.getAttribute("src"), "chrome://browser/skin/devtools/tool-webconsole.svg", "The icon has been set correctly"); + + info ("About to set project with new options."); + yield projecteditor.setProjectToAppPath(TEMP_PATH, { + name: "Test4", + iconUrl: "chrome://browser/skin/devtools/tool-debugger.svg", + projectOverviewURL: SAMPLE_WEBAPP_URL, + validationStatus: "valid" + }); + + ok (!nameLabel.parentNode, "The old elements have been removed"); + + info ("Getting ahold of and validating the project header DOM"); + let image = header.querySelector(".project-image"); + let nameLabel = header.querySelector(".project-name-label"); + let statusElement = header.querySelector(".project-status"); + is (statusElement.getAttribute("status"), "valid", "The status has been set correctly."); + is (nameLabel.textContent, "Test4", "The name label has been set correctly"); + is (image.getAttribute("src"), "chrome://browser/skin/devtools/tool-debugger.svg", "The icon has been set correctly"); + + info ("Test finished, cleaning up"); + projecteditor.project.off("resource-added", failIfResourceAdded); +}); + +function failIfResourceAdded() { + ok (false, "A resource has been added, but it shouldn't have been"); +} diff --git a/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js b/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js index db8b5e2591b7..b31d446012c7 100644 --- a/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js +++ b/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js @@ -7,7 +7,7 @@ // Test ProjectEditor basic functionality let test = asyncTest(function*() { let projecteditor = yield addProjectEditorTabForTempDirectory(); - let TEMP_PATH = [...projecteditor.project.allPaths()][0]; + let TEMP_PATH = projecteditor.project.allPaths()[0]; is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly."); diff --git a/browser/devtools/projecteditor/test/browser_projecteditor_stores.js b/browser/devtools/projecteditor/test/browser_projecteditor_stores.js index 08b356af73f3..8c375ea83786 100644 --- a/browser/devtools/projecteditor/test/browser_projecteditor_stores.js +++ b/browser/devtools/projecteditor/test/browser_projecteditor_stores.js @@ -7,10 +7,10 @@ // Test ProjectEditor basic functionality let test = asyncTest(function*() { let projecteditor = yield addProjectEditorTabForTempDirectory(); - let TEMP_PATH = [...projecteditor.project.allPaths()][0]; + let TEMP_PATH = projecteditor.project.allPaths()[0]; is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly."); - is ([...projecteditor.project.allPaths()].length, 1, "1 path is set"); + is (projecteditor.project.allPaths().length, 1, "1 path is set"); projecteditor.project.removeAllStores(); - is ([...projecteditor.project.allPaths()].length, 0, "No paths are remaining"); + is (projecteditor.project.allPaths().length, 0, "No paths are remaining"); }); diff --git a/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js b/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js index 3835fab0b724..9d23c9793545 100644 --- a/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js +++ b/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js @@ -8,7 +8,7 @@ let test = asyncTest(function*() { let projecteditor = yield addProjectEditorTabForTempDirectory(); - let TEMP_PATH = [...projecteditor.project.allPaths()][0]; + let TEMP_PATH = projecteditor.project.allPaths()[0]; is (getTempFile("").path, TEMP_PATH, "Temp path is set correctly."); diff --git a/browser/themes/shared/devtools/projecteditor/projecteditor.css b/browser/themes/shared/devtools/projecteditor/projecteditor.css index 1d11f603bbe8..6fa7fd43e03c 100644 --- a/browser/themes/shared/devtools/projecteditor/projecteditor.css +++ b/browser/themes/shared/devtools/projecteditor/projecteditor.css @@ -11,6 +11,20 @@ display: none; } +.arrow { + -moz-appearance: treetwisty; + width: 20px; + height: 20px; +} + +.arrow[open] { + -moz-appearance: treetwistyopen; +} + +.arrow[invisible] { + visibility: hidden; +} + #projecteditor-menubar { /* XXX: Hide menu bar until we have option to add menu items to an existing one. */ @@ -27,7 +41,13 @@ .sources-tree { overflow:auto; + overflow-x: hidden; -moz-user-focus: normal; + + /* Allows this to expand inside of parent xul element, while + still supporting child flexbox elements, including ellipses. */ + -moz-box-flex: 1; + display: block; } .sources-tree input { @@ -37,94 +57,95 @@ #main-deck .sources-tree { background: rgb(225, 225, 225); - min-width: 50px; + min-width: 100px; } -#main-deck .sources-tree .side-menu-widget-item { +.entry { color: #18191A; + display: flex; + align-items: center; } -#main-deck .sources-tree .side-menu-widget-item .file-label { - vertical-align: middle; - display: inline-block; +.entry .file-label { + display: flex; + flex: 1; + align-items: center; } -#main-deck .sources-tree .side-menu-widget-item .file-icon { +.entry .file-icon { display: inline-block; background: url(file-icons-sheet@2x.png); background-size: 140px 15px; background-repeat: no-repeat; width: 20px; height: 15px; - vertical-align: middle; background-position: -40px 0; + flex-shrink: 0; } -#main-deck .sources-tree .side-menu-widget-item .file-icon.icon-none { +.entry .file-icon.icon-none { display: none; } -#main-deck .sources-tree .side-menu-widget-item .icon-css { +.entry .icon-css { background-position: 0 0; } -#main-deck .sources-tree .side-menu-widget-item .icon-js { +.entry .icon-js { background-position: -20px 0; } -#main-deck .sources-tree .side-menu-widget-item .icon-html { +.entry .icon-html { background-position: -40px 0; } -#main-deck .sources-tree .side-menu-widget-item .icon-file { +.entry .icon-file { background-position: -60px 0; } -#main-deck .sources-tree .side-menu-widget-item .icon-folder { +.entry .icon-folder { background-position: -80px 0; } -#main-deck .sources-tree .side-menu-widget-item .icon-img { +.entry .icon-img { background-position: -100px 0; } -#main-deck .sources-tree .side-menu-widget-item .icon-manifest { +.entry .icon-manifest { background-position: -120px 0; } -#main-deck .sources-tree .side-menu-widget-item:hover { - background: rgba(0, 0, 0, .05); +.entry { + border: none; + box-shadow: none; + white-space: nowrap; cursor: pointer; } -#main-deck .sources-tree .side-menu-widget-item { - border: none; - box-shadow: none; - line-height: 20px; - vertical-align: middle; - white-space: nowrap; +.entry:hover:not(.entry-group-title):not(.selected) { + background: rgba(0, 0, 0, .05); } -#main-deck .sources-tree .side-menu-widget-item.selected { - background: #3875D7; +.entry.selected { + background: rgba(56, 117, 215, 1); color: #F5F7FA; outline: none; } -#main-deck .sources-tree .side-menu-widget-group-title, -#main-deck .sources-tree .side-menu-widget-group-title:hover:not(.selected) { - background: #B4D7EB; - color: #222; +.entry-group-title { + background: rgba(56, 117, 215, 0.8); + color: #F5F7FA; font-weight: bold; font-size: 1.05em; - cursor: default; line-height: 35px; + padding: 0 10px; } -#main-deck .sources-tree li.child:only-child .side-menu-widget-group-title .expander { +.sources-tree .entry-group-title .expander { display: none; } -#main-deck .sources-tree .side-menu-widget-item .expander { + +.entry .expander { width: 16px; padding: 0; } @@ -143,30 +164,62 @@ padding: 0 3px; } +/* App Manager */ .project-name-label { font-weight: bold; padding-left: 10px; + overflow: hidden; + text-overflow: ellipsis; } -.project-version-label { - color: #666; - padding-left: 5px; - font-size: .9em; +.project-flex { + flex: 1; } .project-image { - max-height: 28px; - margin-left: -.5em; - vertical-align: middle; + max-height: 25px; + margin-left: -10px; } -.editor-image { - padding: 10px; +.project-image, +.project-status, +.project-options { + flex-shrink: 0; } +.project-status { + width: 10px; + height: 10px; + border-radius: 50%; + border: solid 1px rgba(255, 255, 255, .5); + margin-right: 10px; + visibility: hidden; +} + +.project-status[status=valid] { + background: #70bf53; + visibility: visible; +} + +.project-status[status=warning] { + background: #d99b28; + visibility: visible; +} + +.project-status[status=error] { + background: #ed2655; + visibility: visible; +} + +/* Status Bar */ .projecteditor-file-label { font-weight: bold; padding-left: 29px; - vertical-align: middle; + padding-right: 10px; + flex: 1; } +/* Image View */ +.editor-image { + padding: 10px; +}