Bug 1146542 - Restore tab list changes for non-sidebar case. r=ochameau
authorJ. Ryan Stinnett <jryans@gmail.com>
Sun, 29 Mar 2015 23:18:42 -0500
changeset 266750 5dfc78816549039788ff4d10070c1aa2f9164a74
parent 266749 0cabefa3e330953d2a738e5285ea2861fab7dda8
child 266773 6082a98d38613b236758b852ad751b7b454c79c0
child 267850 1e3c6737641029def73f7e82a60ef90b535f469d
push id830
push userraliiev@mozilla.com
push dateFri, 19 Jun 2015 19:24:37 +0000
treeherdermozilla-release@932614382a68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersochameau
bugs1146542
milestone39.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 1146542 - Restore tab list changes for non-sidebar case. r=ochameau
browser/devtools/webide/content/project-listing.js
browser/devtools/webide/content/webide.js
browser/devtools/webide/modules/app-manager.js
browser/devtools/webide/modules/project-list.js
browser/devtools/webide/modules/tab-store.js
browser/devtools/webide/test/browser_tabs.js
browser/devtools/webide/test/sidebars/test_runtime.html
browser/devtools/webide/test/test_autoconnect_runtime.html
browser/devtools/webide/test/test_fullscreenToolbox.html
browser/devtools/webide/test/test_runtime.html
--- a/browser/devtools/webide/content/project-listing.js
+++ b/browser/devtools/webide/content/project-listing.js
@@ -19,24 +19,24 @@ window.addEventListener("load", function
 }, true);
 
 window.addEventListener("unload", function onUnload() {
   window.removeEventListener("unload", onUnload);
   projectList = null;
   AppManager.off("app-manager-update", onAppManagerUpdate);
 });
 
-function onAppManagerUpdate(event, what) {
+function onAppManagerUpdate(event, what, details) {
   switch (what) {
     case "runtime-global-actors":
-    case "runtime-apps-found":
+    case "runtime-targets":
     case "project-validated":
     case "project-removed":
     case "project":
-      projectList.update();
+      projectList.update(details);
       break;
   }
 }
 
 function CreateNewApp() {
   projectList.newApp();
 }
 
--- a/browser/devtools/webide/content/webide.js
+++ b/browser/devtools/webide/content/webide.js
@@ -199,23 +199,24 @@ let UI = {
         break;
       case "project-validated":
         this.updateTitle();
         this.updateCommands();
         this.updateProjectButton();
         this.updateProjectEditorHeader();
         projectList.update();
         break;
+      case "runtime-targets":
       case "project-removed":
-        projectList.update();
+        projectList.update(details);
         break;
       case "install-progress":
         this.updateProgress(Math.round(100 * details.bytesSent / details.totalBytes));
         break;
-      case "runtime-apps-found":
+      case "runtime-targets":
         this.autoSelectProject();
         projectList.update();
         break;
       case "pre-package":
         this.prePackageLog(details);
         break;
     };
     this._updatePromise = promise.resolve();
@@ -1072,16 +1073,28 @@ let Cmds = {
   },
 
   importHostedApp: function(location) {
     projectList.importHostedApp(location);
   },
 
   showProjectPanel: function() {
     ProjectPanel.toggle(projectList.sidebarsEnabled, true);
+
+    // There are currently no available events to listen for when an unselected
+    // tab navigates.  Since we show every tab's location in the project menu,
+    // we re-list all the tabs each time the menu is displayed.
+    // TODO: An event-based solution will be needed for the sidebar UI.
+    if (!projectList.sidebarsEnabled && AppManager.connected) {
+      return AppManager.listTabs().then(() => {
+        projectList.updateTabs();
+      }).catch(console.error);
+    }
+
+    return promise.resolve();
   },
 
   showRuntimePanel: function() {
     RuntimeScanners.scan();
 
     let panel = document.querySelector("#runtime-panel");
     let anchor = document.querySelector("#runtime-panel-button > .panel-button-anchor");
 
--- a/browser/devtools/webide/modules/app-manager.js
+++ b/browser/devtools/webide/modules/app-manager.js
@@ -43,18 +43,20 @@ let AppManager = exports.AppManager = {
     this._initialized = true;
 
     let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
     this.connection = ConnectionManager.createConnection("localhost", port);
     this.onConnectionChanged = this.onConnectionChanged.bind(this);
     this.connection.on(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
 
     this.tabStore = new TabStore(this.connection);
+    this.onTabList = this.onTabList.bind(this);
     this.onTabNavigate = this.onTabNavigate.bind(this);
     this.onTabClosed = this.onTabClosed.bind(this);
+    this.tabStore.on("tab-list", this.onTabList);
     this.tabStore.on("navigate", this.onTabNavigate);
     this.tabStore.on("closed", this.onTabClosed);
 
     this._clearRuntimeList();
     this._rebuildRuntimeList = this._rebuildRuntimeList.bind(this);
     RuntimeScanners.on("runtime-list-updated", this._rebuildRuntimeList);
     RuntimeScanners.enable();
     this._rebuildRuntimeList();
@@ -70,16 +72,17 @@ let AppManager = exports.AppManager = {
     }
     this._initialized = false;
 
     this.selectedProject = null;
     this.selectedRuntime = null;
     RuntimeScanners.off("runtime-list-updated", this._rebuildRuntimeList);
     RuntimeScanners.disable();
     this.runtimeList = null;
+    this.tabStore.off("tab-list", this.onTabList);
     this.tabStore.off("navigate", this.onTabNavigate);
     this.tabStore.off("closed", this.onTabClosed);
     this.tabStore.destroy();
     this.tabStore = null;
     this.connection.off(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
     this._listTabsResponse = null;
     this.connection.disconnect();
     this.connection = null;
@@ -113,24 +116,28 @@ let AppManager = exports.AppManager = {
    *     many pieces of metadata about the project are refreshed, including its
    *     name, manifest details, etc.
    *   runtime:
    *     The selected runtime has changed.
    *   runtime-global-actors:
    *     The list of global actors for the entire runtime (but not actors for a
    *     specific tab or app) are now available, so we can test for features
    *     like preferences and settings.
-   *   runtime-apps-found:
-   *     The list of installed apps has been retreived from the runtime.
    *   runtime-details:
    *     The selected runtime's details have changed, such as its user-visible
    *     name.
    *   runtime-list:
    *     The list of available runtimes has changed, or any of the user-visible
    *     details (like names) for the non-selected runtimes has changed.
+   *   runtime-targets:
+   *     The list of remote runtime targets available from the currently
+   *     connected runtime (such as tabs or apps) has changed, or any of the
+   *     user-visible details (like names) for the non-selected runtime targets
+   *     has changed.  This event includes |type| in the details, to distguish
+   *     "apps" and "tabs".
    */
   update: function(what, details) {
     // Anything we want to forward to the UI
     this.emit("app-manager-update", what, details);
   },
 
   reportError: function(l10nProperty, ...l10nArgs) {
     let win = Services.wm.getMostRecentWindow("devtools:webide");
@@ -173,17 +180,17 @@ let AppManager = exports.AppManager = {
             // in the apps actor require watchApps to be called
             // first.
             this._appsFront = front;
             this._listTabsResponse = response;
             this.update("runtime-global-actors");
           })
           .then(() => {
             this.checkIfProjectIsRunning();
-            this.update("runtime-apps-found");
+            this.update("runtime-targets", { type: "apps" });
             front.fetchIcons();
           });
         } else {
           this._listTabsResponse = response;
           this.update("runtime-global-actors");
         }
       });
     }
@@ -227,18 +234,23 @@ let AppManager = exports.AppManager = {
       }
     }
   },
 
   listTabs: function() {
     return this.tabStore.listTabs();
   },
 
+  onTabList: function() {
+    this.update("runtime-targets", { type: "tabs" });
+  },
+
   // TODO: Merge this into TabProject as part of project-agnostic work
   onTabNavigate: function() {
+    this.update("runtime-targets", { type: "tabs" });
     if (this.selectedProject.type !== "tab") {
       return;
     }
     let tab = this.selectedProject.app = this.tabStore.selectedTab;
     let uri = NetUtil.newURI(tab.url);
     // 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
--- a/browser/devtools/webide/modules/project-list.js
+++ b/browser/devtools/webide/modules/project-list.js
@@ -112,32 +112,37 @@ ProjectList.prototype = {
       opts.panel.appendChild(icon);
       opts.panel.appendChild(span);
     } else {
       opts.panel.setAttribute("label", opts.name);
       opts.panel.setAttribute("image", opts.icon);
     }
   },
 
-  _buildProjectPanelTabs: function() {
-    let tabs = AppManager.tabStore.tabs;
+  updateTabs: function() {
     let tabsHeaderNode = this._doc.querySelector("#panel-header-tabs");
-
-    if (AppManager.connected && tabs.length > 0) {
-      tabsHeaderNode.removeAttribute("hidden");
-    } else {
-      tabsHeaderNode.setAttribute("hidden", "true");
-    }
-
     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;
+
+    if (tabs.length > 0) {
+      tabsHeaderNode.removeAttribute("hidden");
+    } else {
+      tabsHeaderNode.setAttribute("hidden", "true");
+    }
+
     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.
@@ -168,64 +173,22 @@ ProjectList.prototype = {
           type: "tab",
           app: tab,
           icon: tab.favicon,
           location: tab.url,
           name: tab.name
         };
       }, true);
     }
+
+    return promise.resolve();
   },
 
-  update: function() {
-    let deferred = promise.defer();
+  updateApps: function() {
     let doc = this._doc;
-    let panelVboxNode = doc.querySelector("#project-panel > #project-panel-box");
-    let anchorNode = doc.querySelector("#project-panel-button > .panel-button-anchor");
-    let projectsNode = doc.querySelector("#project-panel-projects");
-
-    while (projectsNode.hasChildNodes()) {
-      projectsNode.firstChild.remove();
-    }
-
-    AppProjects.load().then(() => {
-      let projects = AppProjects.store.object.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);
-        this._renderProjectItem({
-          panel: panelItemNode,
-          name: project.name || AppManager.DEFAULT_PROJECT_NAME,
-          icon: project.icon|| AppManager.DEFAULT_PROJECT_ICON
-        });
-        if (!project.name || !project.icon) {
-          // 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
-            });
-          });
-        }
-        panelItemNode.addEventListener("click", () => {
-          if (!this._sidebarsEnabled) {
-            this._UI.hidePanels();
-          }
-          AppManager.selectedProject = project;
-        }, true);
-      }
-
-      deferred.resolve();
-    }, deferred.reject);
-
     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;
     });
@@ -280,22 +243,85 @@ ProjectList.prototype = {
           type: "runtimeApp",
           app: app.manifest,
           icon: app.iconURL,
           name: app.manifest.name
         };
       }, true);
     }
 
+    return promise.resolve();
+  },
+
+  /**
+   * 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.store.object.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);
+        this._renderProjectItem({
+          panel: panelItemNode,
+          name: project.name || AppManager.DEFAULT_PROJECT_NAME,
+          icon: project.icon || AppManager.DEFAULT_PROJECT_ICON
+        });
+        if (!project.name || !project.icon) {
+          // 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
+            });
+          });
+        }
+        panelItemNode.addEventListener("click", () => {
+          if (!this._sidebarsEnabled) {
+            this._UI.hidePanels();
+          }
+          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._buildProjectPanelTabs();
+    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._buildProjectPanelTabs();
+        this.updateTabs();
       }).catch(console.error);
     }
 
     return deferred.promise;
   }
 };
--- a/browser/devtools/webide/modules/tab-store.js
+++ b/browser/devtools/webide/modules/tab-store.js
@@ -1,18 +1,17 @@
 /* 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 EventEmitter = require("devtools/toolkit/event-emitter");
 const { Connection } = require("devtools/client/connection-manager");
-const { Promise: promise } =
-  Cu.import("resource://gre/modules/Promise.jsm", {});
+const promise = require("promise");
 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const { devtools } =
   Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 
 const _knownTabStores = new WeakMap();
 
 let TabStore;
 
@@ -77,17 +76,18 @@ TabStore.prototype = {
         this._connection.client.removeListener("tabNavigated",
                                                this._onTabNavigated);
       }
       this._resetStore();
     }
   },
 
   _onTabListChanged: function() {
-    this.listTabs();
+    this.listTabs().then(() => this.emit("tab-list"))
+                   .catch(console.error);
   },
 
   _onTabNavigated: function(e, { from, title, url }) {
     if (!this._selectedTab || from !== this._selectedTab.actor) {
       return;
     }
     this._selectedTab.url = url;
     this._selectedTab.title = title;
@@ -100,19 +100,23 @@ TabStore.prototype = {
     }
     let deferred = promise.defer();
     this._connection.client.listTabs(response => {
       if (response.error) {
         this._connection.disconnect();
         deferred.reject(response.error);
         return;
       }
+      let tabsChanged = JSON.stringify(this.tabs) !== JSON.stringify(response.tabs);
       this.response = response;
       this.tabs = response.tabs;
       this._checkSelectedTab();
+      if (tabsChanged) {
+        this.emit("tab-list");
+      }
       deferred.resolve(response);
     });
     return deferred.promise;
   },
 
   // TODO: Tab "selection" should really take place by creating a TabProject
   // which is the selected project.  This should be done as part of the
   // project-agnostic work.
--- a/browser/devtools/webide/test/browser_tabs.js
+++ b/browser/devtools/webide/test/browser_tabs.js
@@ -27,33 +27,40 @@ function test() {
     is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
 
     yield selectTabProject(win);
 
     let project = win.AppManager.selectedProject;
     is(project.location, TEST_URI, "Location is correct");
     is(project.name, "example.com: Test Tab", "Name is correct");
 
+    // Ensure tab list changes are noticed
+    let tabsNode = win.document.querySelector("#project-panel-tabs");
+    is(tabsNode.querySelectorAll(".panel-item").length, 2, "2 tabs available");
+    yield removeTab(tab);
+    yield waitForUpdate(win, "runtime-targets");
+    is(tabsNode.querySelectorAll(".panel-item").length, 1, "1 tab available");
+
+    yield win.Cmds.disconnectRuntime();
     yield closeWebIDE(win);
+
     DebuggerServer.destroy();
-    yield removeTab(tab);
   }).then(finish, handleError);
 }
 
 function connectToLocal(win) {
   let deferred = promise.defer();
   win.AppManager.connection.once(
       win.Connection.Events.CONNECTED,
       () => deferred.resolve());
   win.document.querySelectorAll(".runtime-panel-item-other")[1].click();
   return deferred.promise;
 }
 
 function selectTabProject(win) {
   return Task.spawn(function() {
-    yield win.AppManager.listTabs();
-    win.projectList.update();
-    yield nextTick();
+    yield win.Cmds.showProjectPanel();
+    yield waitForUpdate(win, "runtime-targets");
     let tabsNode = win.document.querySelector("#project-panel-tabs");
     let tabNode = tabsNode.querySelectorAll(".panel-item")[1];
     tabNode.click();
   });
 }
--- a/browser/devtools/webide/test/sidebars/test_runtime.html
+++ b/browser/devtools/webide/test/sidebars/test_runtime.html
@@ -81,17 +81,17 @@
 
           items[0].click();
 
           ok(win.document.querySelector("window").className, "busy", "UI is busy");
           yield win.UI._busyPromise;
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
 
-          yield waitForUpdate(win, "runtime-apps-found");
+          yield waitForUpdate(win, "runtime-targets");
 
           ok(isPlayActive(), "play button is enabled 1");
           ok(!isStopActive(), "stop button is disabled 1");
           let oldProject = win.AppManager.selectedProject;
           win.AppManager.selectedProject = null;
 
           yield nextTick();
 
@@ -110,17 +110,17 @@
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           ok(win.AppManager.selectedProject, "A project is still selected");
           ok(!isPlayActive(), "play button is disabled 4");
           ok(!isStopActive(), "stop button is disabled 4");
 
           winIframe.document.querySelectorAll(".runtime-panel-item-other")[1].click();
 
-          yield waitForUpdate(win, "runtime-apps-found");
+          yield waitForUpdate(win, "runtime-targets");
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
 
           ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
 
           // Select main process
           yield win.Cmds.showProjectPanel();
           SimpleTest.executeSoon(() => {
--- a/browser/devtools/webide/test/test_autoconnect_runtime.html
+++ b/browser/devtools/webide/test/test_autoconnect_runtime.html
@@ -66,17 +66,17 @@
 
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           win = yield openWebIDE();
 
           win.AppManager.runtimeList.usb.push(fakeRuntime);
           win.AppManager.update("runtime-list");
 
-          yield waitForUpdate(win, "runtime-apps-found");
+          yield waitForUpdate(win, "runtime-targets");
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Automatically reconnected");
 
           yield win.Cmds.disconnectRuntime();
 
           yield closeWebIDE(win);
 
           DebuggerServer.destroy();
--- a/browser/devtools/webide/test/test_fullscreenToolbox.html
+++ b/browser/devtools/webide/test/test_fullscreenToolbox.html
@@ -30,20 +30,19 @@
 
         Task.spawn(function* () {
           Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
           win = yield openWebIDE();
           win.AppManager.update("runtime-list");
 
           yield connectToLocal(win);
 
-          yield waitForUpdate(win, "runtime-apps-found");
-
           // Select main process
           yield win.Cmds.showProjectPanel();
+          yield waitForUpdate(win, "runtime-targets");
           SimpleTest.executeSoon(() => {
             win.document.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
           });
 
           yield waitForUpdate(win, "project");
 
           ok(win.UI.toolboxPromise, "Toolbox promise exists");
           yield win.UI.toolboxPromise;
--- a/browser/devtools/webide/test/test_runtime.html
+++ b/browser/devtools/webide/test/test_runtime.html
@@ -104,17 +104,17 @@
 
           items[0].click();
 
           ok(win.document.querySelector("window").className, "busy", "UI is busy");
           yield win.UI._busyPromise;
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
 
-          yield waitForUpdate(win, "runtime-apps-found");
+          yield waitForUpdate(win, "runtime-global-actors");
 
           ok(isPlayActive(), "play button is enabled 1");
           ok(!isStopActive(), "stop button is disabled 1");
           let oldProject = win.AppManager.selectedProject;
           win.AppManager.selectedProject = null;
 
           yield nextTick();
 
@@ -133,24 +133,25 @@
           is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
 
           ok(win.AppManager.selectedProject, "A project is still selected");
           ok(!isPlayActive(), "play button is disabled 4");
           ok(!isStopActive(), "stop button is disabled 4");
 
           win.document.querySelectorAll(".runtime-panel-item-other")[1].click();
 
-          yield waitForUpdate(win, "runtime-apps-found");
+          yield waitForUpdate(win, "runtime-targets");
 
           is(Object.keys(DebuggerServer._connections).length, 1, "Locally connected");
 
           ok(win.AppManager.isMainProcessDebuggable(), "Main process available");
 
           // Select main process
           yield win.Cmds.showProjectPanel();
+          yield waitForUpdate(win, "runtime-targets");
           SimpleTest.executeSoon(() => {
             win.document.querySelectorAll("#project-panel-runtimeapps .panel-item")[0].click();
           });
 
           yield waitForUpdate(win, "project");
 
           // Toolbox opens automatically for main process / runtime apps
           ok(win.UI.toolboxPromise, "Toolbox promise exists");