Bug 1019676 - Project editor: Allow app header to be updated and add gear icon / status indicator. r=harth
authorBrian Grinstead <bgrinstead@mozilla.com>
Thu, 05 Jun 2014 13:38:00 -0400
changeset 206411 939350a04d9feae944ff8898ba8caffeeb5d312c
parent 206410 1023a50167c5957c5924f9881703fe36ce1c6fef
child 206412 8404496a62ee48e359d1465a0d08b3e3f8de5753
push id3741
push userasasaki@mozilla.com
push dateMon, 21 Jul 2014 20:25:18 +0000
treeherdermozilla-beta@4d6f46f5af68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersharth
bugs1019676
milestone32.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1019676 - Project editor: Allow app header to be updated and add gear icon / status indicator. r=harth
browser/devtools/projecteditor/chrome/content/projecteditor-loader.js
browser/devtools/projecteditor/chrome/content/projecteditor.xul
browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js
browser/devtools/projecteditor/lib/project.js
browser/devtools/projecteditor/lib/projecteditor.js
browser/devtools/projecteditor/lib/tree.js
browser/devtools/projecteditor/test/browser.ini
browser/devtools/projecteditor/test/browser_projecteditor_app_options.js
browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js
browser/devtools/projecteditor/test/browser_projecteditor_stores.js
browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js
browser/themes/shared/devtools/projecteditor/projecteditor.css
--- a/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor-loader.js
@@ -2,19 +2,19 @@ const Cu = Components.utils;
 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
 const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
 const require = devtools.require;
 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
  * chrome://browser/content/devtools/projecteditor-loader.xul.
  * This emulates the integration points that the app manager uses.
  */
 document.addEventListener("DOMContentLoaded", function onDOMReady(e) {
   document.removeEventListener("DOMContentLoaded", onDOMReady, false);
@@ -51,21 +51,23 @@ document.addEventListener("DOMContentLoa
   projecteditor.on("onCommand", (cmd) => {
     console.log("Command: " + cmd);
   });
 
   projecteditor.loaded.then(() => {
     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);
 
 /**
  * Build a temporary directory as a workspace for this loader
  * https://developer.mozilla.org/en-US/Add-ons/Code_snippets/File_I_O
  */
--- a/browser/devtools/projecteditor/chrome/content/projecteditor.xul
+++ b/browser/devtools/projecteditor/chrome/content/projecteditor.xul
@@ -1,18 +1,16 @@
 <?xml version="1.0"?>
 <!-- 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/. -->
 <?xml-stylesheet href="chrome://browser/skin/devtools/light-theme.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/projecteditor/projecteditor.css" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/content/devtools/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/devtools/debugger.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/common.css" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/skin/devtools/widgets.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/devtools/markup-view.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/devtools/markup-view.css" type="text/css"?>
 
 <?xul-overlay href="chrome://global/content/editMenuOverlay.xul"?>
 
 <!DOCTYPE window [
 <!ENTITY % scratchpadDTD SYSTEM "chrome://browser/locale/devtools/scratchpad.dtd" >
  %scratchpadDTD;
--- a/browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js
+++ b/browser/devtools/projecteditor/lib/plugins/app-manager/plugin.js
@@ -1,15 +1,16 @@
 const { Cu } = require("chrome");
 const { Class } = require("sdk/core/heritage");
 const { EventTarget } = require("sdk/event/target");
 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,
 
   isAppManagerProject: function() {
     return !!this.host.project.appManagerOpts;
   },
   editorForResource: function(resource) {
@@ -20,28 +21,40 @@ var AppManagerRenderer = Class({
   onAnnotate: function(resource, editor, elt) {
     if (resource.parent || !this.isAppManagerProject()) {
       return;
     }
 
     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;
   }
 });
 
 exports.AppManagerRenderer = AppManagerRenderer;
 registerPlugin(AppManagerRenderer);
--- a/browser/devtools/projecteditor/lib/project.js
+++ b/browser/devtools/projecteditor/lib/project.js
@@ -131,23 +131,21 @@ var Project = Class({
     for (let [path, store] of this.localStores) {
       yield store;
     }
   },
 
   /**
    * Get every file path used inside of the project.
    *
-   * @returns generator-iterator<string>
+   * @returns Array<string>
    *          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())];
   },
 
   /**
    * Get the store that contains a path.
    *
    * @returns Store
    *          The store, if any.  Will return null if no store
    *          contains the given path.
--- a/browser/devtools/projecteditor/lib/projecteditor.js
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -259,24 +259,39 @@ var ProjectEditor = Class({
 
   /**
    * Set the current project viewed by the projecteditor to a single path,
    * used by the app manager.
    *
    * @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();
   },
 
   /**
    * Open a resource in a particular shell.
    *
    * @param Resource resource
    *                 The file to be opened.
--- a/browser/devtools/projecteditor/lib/tree.js
+++ b/browser/devtools/projecteditor/lib/tree.js
@@ -34,17 +34,17 @@ var ResourceContainer = Class({
 
     let doc = tree.doc;
 
     this.elt = doc.createElementNS(HTML_NS, "li");
     this.elt.classList.add("child");
 
     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");
 
     this.elt.appendChild(this.line);
 
     this.highlighter = doc.createElementNS(HTML_NS, "span");
     this.highlighter.classList.add("highlighter");
     this.line.appendChild(this.highlighter);
@@ -218,25 +218,24 @@ var TreeView = Class({
     this.options = merge({
       resourceFormatter: function(resource, elt) {
         elt.textContent = resource.toString();
       }
     }, options);
     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);
     this.updateResource = this.updateResource.bind(this);
   },
 
   destroy: function() {
     this._destroyed = true;
@@ -310,17 +309,17 @@ var TreeView = Class({
     this.roots.add(model.root);
     model.root.refresh().then(root => {
       if (this._destroyed || !this.models.has(model)) {
         // model may have been removed during the initial refresh.
         // In this case, do not import the resource or add to DOM, just leave it be.
         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);
 
       this.children.insertBefore(container.elt, placeholder);
       this.children.removeChild(placeholder);
     });
   },
 
--- a/browser/devtools/projecteditor/test/browser.ini
+++ b/browser/devtools/projecteditor/test/browser.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 skip-if = os == "win" && !debug # Bug 1014046
 subsuite = devtools
 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]
 [browser_projecteditor_init.js]
 [browser_projecteditor_new_file.js]
 [browser_projecteditor_stores.js]
 [browser_projecteditor_tree_selection.js]
new file mode 100644
--- /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");
+}
--- a/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_editing_01.js
@@ -2,17 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // 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.");
 
   ok (projecteditor.currentEditor, "There is an editor for projecteditor");
   let resources = projecteditor.project.allResources();
 
   resources.forEach((r, i) => {
     console.log("Resource detected", r.path, i);
--- a/browser/devtools/projecteditor/test/browser_projecteditor_stores.js
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_stores.js
@@ -2,15 +2,15 @@
 /* Any copyright is dedicated to the Public Domain.
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // 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");
 });
--- a/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js
+++ b/browser/devtools/projecteditor/test/browser_projecteditor_tree_selection.js
@@ -3,17 +3,17 @@
  http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test tree selection 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.");
 
   ok (projecteditor.currentEditor, "There is an editor for projecteditor");
   let resources = projecteditor.project.allResources();
 
   is (
     resources.map(r=>r.basename).join("|"),
--- a/browser/themes/shared/devtools/projecteditor/projecteditor.css
+++ b/browser/themes/shared/devtools/projecteditor/projecteditor.css
@@ -6,130 +6,151 @@
  :root {
   color: #18191a;
 }
 
 .plugin-hidden {
   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. */
   display: none;
 }
 
 #projecteditor-toolbar,
 #projecteditor-toolbar-bottom {
   display: none; /* For now don't show the status bars */
   min-height: 22px;
   height: 22px;
   background: rgb(237, 237, 237);
 }
 
 .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 {
   margin: 2px;
   border: 1px solid gray;
 }
 
 #main-deck .sources-tree {
   background: rgb(225, 225, 225);
-  min-width: 50px;
-}
-
-#main-deck .sources-tree .side-menu-widget-item {
-  color: #18191A;
+  min-width: 100px;
 }
 
-#main-deck .sources-tree .side-menu-widget-item .file-label {
-  vertical-align: middle;
-  display: inline-block;
+.entry {
+  color: #18191A;
+  display: flex;
+  align-items: center;
 }
 
-#main-deck .sources-tree .side-menu-widget-item .file-icon {
+.entry .file-label {
+  display: flex;
+  flex: 1;
+  align-items: center;
+}
+
+.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;
 }
 
 .tree-collapsed .children {
   display: none;
 }
 
@@ -138,35 +159,67 @@
 #projecteditor-toolbar textbox {
   margin: 0;
 }
 
 .projecteditor-basic-display {
   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;
+}
+
+.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;
+  padding-right: 10px;
+  flex: 1;
+}
+
+/* Image View */
 .editor-image {
   padding: 10px;
 }
-
-.projecteditor-file-label {
-  font-weight: bold;
-  padding-left: 29px;
-  vertical-align: middle;
-}
-