Bug 1463555 - Queue adding the tabs to the inspector sidebar. r=Honza draft
authorGabriel Luong <gabriel.luong@gmail.com>
Wed, 23 May 2018 13:31:02 -0400
changeset 798911 d83de1b1a9513e2eed36e1d8c3f2aaa08e35495b
parent 798910 55bf0e045b5e3ab006e84d7c17d98871714fed94
push id110872
push userbmo:gl@mozilla.com
push dateWed, 23 May 2018 17:32:10 +0000
reviewersHonza
bugs1463555
milestone62.0a1
Bug 1463555 - Queue adding the tabs to the inspector sidebar. r=Honza MozReview-Commit-ID: IoKLFVH03AZ
devtools/client/inspector/inspector.js
devtools/client/inspector/toolsidebar.js
devtools/client/shared/components/tabs/TabBar.js
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -724,17 +724,17 @@ Inspector.prototype = {
       this.sidebarSplitBox.setState({
         endPanelControl: false,
         splitterSize: 0,
       });
 
       this.ruleViewSideBar.hide();
       await this.ruleViewSideBar.removeTab("ruleview");
 
-      this.sidebar.addExistingTab(
+      this.sidebar.queueExistingTab(
         "ruleview",
         INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
         defaultTab == "ruleview",
         0);
     }
 
     this.emit("ruleview-added");
   },
@@ -804,17 +804,17 @@ Inspector.prototype = {
     // Append all side panels
 
     await this.addRuleView(defaultTab);
 
     // Inject a lazy loaded react tab by exposing a fake React object
     // with a lazy defined Tab thanks to `panel` being a function
     let layoutId = "layoutview";
     let layoutTitle = INSPECTOR_L10N.getStr("inspector.sidebar.layoutViewTitle2");
-    this.sidebar.addTab(
+    this.sidebar.queueTab(
       layoutId,
       layoutTitle,
       {
         props: {
           id: layoutId,
           title: layoutTitle
         },
         panel: () => {
@@ -824,56 +824,56 @@ Inspector.prototype = {
             this.layoutview = new LayoutView(this, this.panelWin);
           }
 
           return this.layoutview.provider;
         }
       },
       defaultTab == layoutId);
 
-    this.sidebar.addExistingTab(
+    this.sidebar.queueExistingTab(
       "computedview",
       INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
       defaultTab == "computedview");
 
     const animationTitle =
       INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle");
 
     if (Services.prefs.getBoolPref("devtools.new-animationinspector.enabled")) {
       const animationId = "newanimationinspector";
 
-      this.sidebar.addTab(
+      this.sidebar.queueTab(
         animationId,
         animationTitle,
         {
           props: {
             id: animationId,
             title: animationTitle
           },
           panel: () => {
             const AnimationInspector =
               this.browserRequire("devtools/client/inspector/animation/animation");
             this.animationinspector = new AnimationInspector(this, this.panelWin);
             return this.animationinspector.provider;
           }
         },
         defaultTab == animationId);
     } else {
-      this.sidebar.addFrameTab(
+      this.sidebar.queueFrameTab(
         "animationinspector",
         animationTitle,
         "chrome://devtools/content/inspector/animation-old/animation-inspector.xhtml",
         defaultTab == "animationinspector");
     }
 
     // Inject a lazy loaded react tab by exposing a fake React object
     // with a lazy defined Tab thanks to `panel` being a function
     let fontId = "fontinspector";
     let fontTitle = INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle");
-    this.sidebar.addTab(
+    this.sidebar.queueTab(
       fontId,
       fontTitle,
       {
         props: {
           id: fontId,
           title: fontTitle
         },
         panel: () => {
@@ -883,16 +883,18 @@ Inspector.prototype = {
             this.fontinspector = new FontInspector(this, this.panelWin);
           }
 
           return this.fontinspector.provider;
         }
       },
       defaultTab == fontId);
 
+    this.sidebar.addAllQueuedTabs();
+
     // Persist splitter state in preferences.
     this.sidebar.on("show", this.onSidebarShown);
     this.sidebar.on("hide", this.onSidebarHidden);
     this.sidebar.on("destroy", this.onSidebarHidden);
 
     this.sidebar.show(defaultTab);
   },
 
--- a/devtools/client/inspector/toolsidebar.js
+++ b/devtools/client/inspector/toolsidebar.js
@@ -80,16 +80,23 @@ ToolSidebar.prototype = {
       sidebarToggleButton: this._options.sidebarToggleButton,
       onSelect: this.handleSelectionChange.bind(this),
     });
 
     this._tabbar = this.ReactDOM.render(sidebar, this._tabbox);
   },
 
   /**
+   * Adds all the queued tabs.
+   */
+  addAllQueuedTabs: function() {
+    this._tabbar.addAllQueuedTabs();
+  },
+
+  /**
    * Register a side-panel tab.
    *
    * @param {String} tab uniq id
    * @param {String} title tab title
    * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
    * @param {Boolean} selected true if the panel should be selected
    * @param {Number} index the position where the tab should be inserted
    */
@@ -138,16 +145,75 @@ ToolSidebar.prototype = {
       url: url,
       onMount: this.onSidePanelMounted.bind(this),
       onUnmount: this.onSidePanelUnmounted.bind(this),
     });
 
     this.addTab(id, title, panel, selected, index);
   },
 
+  /**
+   * Queues a side-panel tab to be added..
+   *
+   * @param {String} tab uniq id
+   * @param {String} title tab title
+   * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
+   * @param {Boolean} selected true if the panel should be selected
+   * @param {Number} index the position where the tab should be inserted
+   */
+  queueTab: function(id, title, panel, selected, index) {
+    this._tabbar.queueTab(id, title, selected, panel, null, index);
+    this.emit("new-tab-registered", id);
+  },
+
+  /**
+   * Helper API for queuing side-panels that use existing DOM nodes
+   * (defined within inspector.xhtml) as the content.
+   *
+   * @param {String} tab uniq id
+   * @param {String} title tab title
+   * @param {Boolean} selected true if the panel should be selected
+   * @param {Number} index the position where the tab should be inserted
+   */
+  queueExistingTab: function(id, title, selected, index) {
+    let panel = this.InspectorTabPanel({
+      id: id,
+      idPrefix: this.TABPANEL_ID_PREFIX,
+      key: id,
+      title: title,
+    });
+
+    this.queueTab(id, title, panel, selected, index);
+  },
+
+  /**
+   * Helper API for queuing side-panels that use existing <iframe> nodes
+   * (defined within inspector.xhtml) as the content.
+   * The document must have a title, which will be used as the name of the tab.
+   *
+   * @param {String} tab uniq id
+   * @param {String} title tab title
+   * @param {String} url
+   * @param {Boolean} selected true if the panel should be selected
+   * @param {Number} index the position where the tab should be inserted
+   */
+  queueFrameTab: function(id, title, url, selected, index) {
+    let panel = this.InspectorTabPanel({
+      id: id,
+      idPrefix: this.TABPANEL_ID_PREFIX,
+      key: id,
+      title: title,
+      url: url,
+      onMount: this.onSidePanelMounted.bind(this),
+      onUnmount: this.onSidePanelUnmounted.bind(this),
+    });
+
+    this.queueTab(id, title, panel, selected, index);
+  },
+
   onSidePanelMounted: function(content, props) {
     let iframe = content.querySelector("iframe");
     if (!iframe || iframe.getAttribute("src")) {
       return;
     }
 
     let onIFrameLoaded = (event) => {
       iframe.removeEventListener("load", onIFrameLoaded, true);
--- a/devtools/client/shared/components/tabs/TabBar.js
+++ b/devtools/client/shared/components/tabs/TabBar.js
@@ -58,18 +58,23 @@ class Tabbar extends Component {
     let tabs = this.createTabs(children);
     let activeTab = tabs.findIndex((tab, index) => tab.id === activeTabId);
 
     this.state = {
       activeTab: activeTab === -1 ? 0 : activeTab,
       tabs,
     };
 
+    // Array of queued tabs to add to the Tabbar.
+    this.queuedTabs = [];
+
     this.createTabs = this.createTabs.bind(this);
     this.addTab = this.addTab.bind(this);
+    this.addAllQueuedTabs = this.addAllQueuedTabs.bind(this);
+    this.queueTab = this.queueTab.bind(this);
     this.toggleTab = this.toggleTab.bind(this);
     this.removeTab = this.removeTab.bind(this);
     this.select = this.select.bind(this);
     this.getTabIndex = this.getTabIndex.bind(this);
     this.getTabId = this.getTabId.bind(this);
     this.getCurrentTabId = this.getCurrentTabId.bind(this);
     this.onTabChanged = this.onTabChanged.bind(this);
     this.onAllTabsMenuClick = this.onAllTabsMenuClick.bind(this);
@@ -109,30 +114,83 @@ class Tabbar extends Component {
 
     if (index >= 0) {
       tabs.splice(index, 0, {id, title, panel, url});
     } else {
       tabs.push({id, title, panel, url});
     }
 
     let newState = Object.assign({}, this.state, {
-      tabs: tabs,
+      tabs,
     });
 
     if (selected) {
       newState.activeTab = index >= 0 ? index : tabs.length - 1;
     }
 
     this.setState(newState, () => {
       if (this.props.onSelect && selected) {
         this.props.onSelect(id);
       }
     });
   }
 
+  addAllQueuedTabs() {
+    if (!this.queuedTabs.length) {
+      return;
+    }
+
+    let tabs = this.state.tabs.slice();
+    let activeId;
+    let activeTab;
+
+    for (let { id, index, panel, selected, title, url } of this.queuedTabs) {
+      if (index >= 0) {
+        tabs.splice(index, 0, {id, title, panel, url});
+      } else {
+        tabs.push({id, title, panel, url});
+      }
+
+      if (selected) {
+        activeId = id;
+        activeTab = index >= 0 ? index : tabs.length - 1;
+      }
+    }
+
+    let newState = Object.assign({}, this.state, {
+      activeTab,
+      tabs,
+    });
+
+    this.setState(newState, () => {
+      if (this.props.onSelect) {
+        this.props.onSelect(activeId);
+      }
+    });
+
+    this.queuedTabs = [];
+  }
+
+  /**
+   * Queues a tab to be added. This is more performant than calling addTab for every
+   * single tab to be added since we will limit the number of renders happening when
+   * a new state is set. Once all the tabs to be added have been queued, call
+   * addAllQueuedTabs() to populate the TabBar with all the queued tabs.
+   */
+  queueTab(id, title, selected = false, panel, url, index = -1) {
+    this.queuedTabs.push({
+      id,
+      index,
+      panel,
+      selected,
+      title,
+      url,
+    });
+  }
+
   toggleTab(tabId, isVisible) {
     let index = this.getTabIndex(tabId);
     if (index < 0) {
       return;
     }
 
     let tabs = this.state.tabs.slice();
     tabs[index] = Object.assign({}, tabs[index], {