Bug 1332447 part 2 - Implementation of the tabstrip hidding API. draft
authorEdouard Oger <eoger@fastmail.com>
Sat, 19 Aug 2017 16:48:06 -0400
changeset 673147 1d87a73668e0769ae357a07512417943eab38b3c
parent 673146 cab8c5454801cdd8417a48eacb16effcc0b563c0
child 734016 f4933b36f11b672fe0316bb97abff79feea5a74d
push id82476
push userbmo:eoger@fastmail.com
push dateSun, 01 Oct 2017 21:15:27 +0000
bugs1332447
milestone58.0a1
Bug 1332447 part 2 - Implementation of the tabstrip hidding API. MozReview-Commit-ID: Il6lCTrDqOU
browser/components/extensions/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/locales/en-US/chrome/browser/browser.properties
browser/themes/linux/browser.css
browser/themes/osx/browser.css
browser/themes/windows/browser.css
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -72,16 +72,92 @@ let tabListener = {
         this.initTabReady();
         this.tabReadyPromises.set(nativeTab, deferred);
       }
     }
     return deferred.promise;
   },
 };
 
+let tabsVisilityTracker = new EventEmitter();
+let tabVisibilityManager = {
+  // Set of extension contexts that requested the tabstrip to be hidden.
+  tabHiddingContexts: new Set(),
+
+  get tabsVisibility() {
+    return this.tabHiddingContexts.size == 0;
+  },
+
+  set tabsVisibility(isVisible) {
+    this._updateCurrentWindowsTabsVisibility(isVisible);
+
+    if (!isVisible) {
+      tabsVisilityTracker.emit("visibilityChange", true);
+
+      this._hideTabs = (window) => {
+        // The titlebar height is calculated dynamically, let TabsInTitlebar
+        // do its job first then collapse the tabstrip on the next tick.
+        Promise.resolve().then(() => {
+          this._updateTabsVisibility(window, false);
+        });
+      };
+
+      windowTracker.addOpenListener(this._hideTabs);
+    } else {
+      tabsVisilityTracker.emit("visibilityChange", false);
+      windowTracker.removeOpenListener(this._hideTabs);
+    }
+  },
+
+  _updateCurrentWindowsTabsVisibility(isVisible) {
+    for (let window of windowTracker.browserWindows()) {
+      this._updateTabsVisibility(window, isVisible);
+    }
+  },
+
+  _updateTabsVisibility(window, isVisible) {
+    let toolbar = window.document.getElementById("TabsToolbar");
+    if (isVisible) {
+      toolbar.classList.remove("webext-collapsed");
+    } else {
+      toolbar.classList.add("webext-collapsed");
+    }
+  },
+
+  // Do not call directly. Called by contexts when they shut down.
+  close(context) {
+    this.tabHiddingContexts.delete(context);
+    if (this.tabHiddingContexts.size == 0) {
+      this.tabsVisibility = true;
+    }
+  },
+
+  setTabsVisibility(context, visible) {
+    if (!visible) {
+      if (this.tabHiddingContexts.has(context)) {
+        return;
+      }
+      let wasVisible = this.tabsVisibility;
+      this.tabHiddingContexts.add(context);
+      context.callOnClose(this);
+      // First context asking the tabstrip to be hidden.
+      if (wasVisible) {
+        this.tabsVisibility = false;
+      }
+    } else {
+      // Visible true always takes priority: clear everyone.
+      for (let context of this.tabHiddingContexts) {
+        context.forgetOnClose(this);
+      }
+      this.tabHiddingContexts.clear();
+      this.tabsVisibility = true;
+    }
+  },
+};
+
 this.tabs = class extends ExtensionAPI {
   getAPI(context) {
     let {extension} = context;
 
     let {tabManager} = extension;
 
     function getTabOrActive(tabId) {
       if (tabId !== null) {
@@ -314,16 +390,27 @@ this.tabs = class extends ExtensionAPI {
             windowTracker.removeListener("TabAttrModified", listener);
             windowTracker.removeListener("TabPinned", listener);
             windowTracker.removeListener("TabUnpinned", listener);
             windowTracker.removeListener("TabBrowserInserted", listener);
             tabTracker.off("tab-isarticle", isArticleChangeListener);
           };
         }).api(),
 
+        onVisibilityChanged: new EventManager(context, "tabs.onVisibilityChanged", fire => {
+          let listener = (eventName, hidden) => {
+            fire.async(hidden);
+          };
+
+          tabsVisilityTracker.on("visibilityChange", listener);
+          return () => {
+            tabsVisibilityTracker.off("visibilityChange", listener);
+          };
+        }).api(),
+
         create(createProperties) {
           return new Promise((resolve, reject) => {
             let window = createProperties.windowId !== null ?
               windowTracker.getWindow(createProperties.windowId, context) :
               windowTracker.topWindow;
 
             if (!window.gBrowser) {
               let obs = (finishedWindow, topic, data) => {
@@ -933,13 +1020,21 @@ this.tabs = class extends ExtensionAPI {
           let tab = await promiseTabWhenReady(tabId);
           if (!tab.isInReaderMode && !tab.isArticle) {
             throw new ExtensionError("The specified tab cannot be placed into reader mode.");
           }
           tab = getTabOrActive(tabId);
 
           tab.linkedBrowser.messageManager.sendAsyncMessage("Reader:ToggleReaderMode");
         },
+
+        getTabsVisibility() {
+          return tabVisibilityManager.tabsVisibility;
+        },
+
+        setTabsVisibility(visible) {
+          return tabVisibilityManager.setTabsVisibility(context, visible);
+        }
       },
     };
     return self;
   }
 };
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -7,17 +7,18 @@
     "namespace": "manifest",
     "types": [
       {
         "$extend": "OptionalPermission",
         "choices": [{
           "type": "string",
           "enum": [
             "activeTab",
-            "tabs"
+            "tabs",
+            "tabsVisibility"
           ]
         }]
       }
     ]
   },
   {
     "namespace": "tabs",
     "description": "Use the <code>browser.tabs</code> API to interact with the browser's tab system. You can use this API to create, modify, and rearrange tabs in the browser.",
@@ -1187,16 +1188,44 @@
               {
                 "type": "string",
                 "name": "status",
                 "description": "Save status: saved, replaced, canceled, not_saved, not_replaced."
               }
             ]
           }
         ]
+      },
+      {
+        "name": "getTabsVisibility",
+        "type": "function",
+        "description": "Query the visibility of the tabstrip.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "hidden",
+                "type": "boolean",
+                "description": "True if the tabstrip is hidden."
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "setTabsVisibility",
+        "type": "function",
+        "description": "Sets the visibility of the tabstrip.",
+        "permissions": ["tabsVisibility"],
+        "parameters": [{
+          "type": "boolean"
+        }]
       }
     ],
     "events": [
       {
         "name": "onCreated",
         "type": "function",
         "description": "Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired, but you can listen to onUpdated events to be notified when a URL is set.",
         "parameters": [
@@ -1468,12 +1497,22 @@
           "name": "ZoomChangeInfo",
           "properties": {
             "tabId": {"type": "integer", "minimum": 0},
             "oldZoomFactor": {"type": "number"},
             "newZoomFactor": {"type": "number"},
             "zoomSettings": {"$ref": "ZoomSettings"}
           }
         }]
+      },
+      {
+        "name": "onVisibilityChanged",
+        "type": "function",
+        "description": "Fired when the tabstrip visibility changes.",
+        "parameters": [{
+          "type": "boolean",
+          "name": "hidden",
+          "description": "True if the tabstrip is currently invisible."
+        }]
       }
     ]
   }
 ]
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -114,16 +114,17 @@ webextPerms.description.notifications=Di
 webextPerms.description.pkcs11=Provide cryptographic authentication services
 webextPerms.description.privacy=Read and modify privacy settings
 webextPerms.description.proxy=Control browser proxy settings
 webextPerms.description.sessions=Access recently closed tabs
 webextPerms.description.tabs=Access browser tabs
 webextPerms.description.topSites=Access browsing history
 webextPerms.description.unlimitedStorage=Store unlimited amount of client-side data
 webextPerms.description.webNavigation=Access browser activity during navigation
+webextPerms.description.tabsVisibility=Changes the visibility of the tab strip
 
 webextPerms.hostDescription.allUrls=Access your data for all websites
 
 # LOCALIZATION NOTE (webextPerms.hostDescription.wildcard)
 # %S will be replaced by the DNS domain for which a webextension
 # is requesting access (e.g., mozilla.org)
 webextPerms.hostDescription.wildcard=Access your data for sites in the %S domain
 
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -67,16 +67,20 @@
 #TabsToolbar:not([collapsed="true"]) + #nav-bar {
   border-top: 1px solid var(--tabs-border) !important;
   background-clip: padding-box;
   /* Position the toolbar above the bottom of background tabs */
   position: relative;
   z-index: 1;
 }
 
+#TabsToolbar.webext-collapsed > * {
+  visibility: collapse;
+}
+
 #nav-bar {
   padding-top: 2px;
   padding-bottom: 2px;
 }
 
 #browser-bottombox {
   /* opaque for layers optimization */
   background-color: -moz-Dialog;
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -154,16 +154,24 @@
 }
 
 #TabsToolbar:not([collapsed="true"]) + #nav-bar {
   /* The toolbar buttons that animate are only visible when the #TabsToolbar is not collapsed.
      The animations use position:absolute and require a positioned #nav-bar. */
   position: relative;
 }
 
+#TabsToolbar.webext-collapsed {
+  min-height: 27px;
+}
+
+#TabsToolbar.webext-collapsed > * {
+  visibility: collapse;
+}
+
 #PersonalToolbar:not(:-moz-lwtheme):-moz-window-inactive,
 #nav-bar:not(:-moz-lwtheme):-moz-window-inactive {
   background-color: -moz-mac-chrome-inactive;
 }
 
 /* ----- BOOKMARK TOOLBAR ----- */
 
 #nav-bar-customization-target > #wrapper-personal-bookmarks > #personal-bookmarks {
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -238,16 +238,46 @@
 }
 
 #TabsToolbar:not([collapsed="true"]) + #nav-bar {
   /* Position the toolbar above the bottom of background tabs */
   position: relative;
   z-index: 1;
 }
 
+#TabsToolbar.webext-collapsed > * {
+  visibility: collapse;
+}
+
+#TabsToolbar.webext-collapsed {
+  min-height: 31.5px;
+}
+
+@media (-moz-os-version: windows-win7) {
+  #TabsToolbar.webext-collapsed {
+    min-height: 20px;
+  }
+}
+
+@media (-moz-os-version: windows-win8) {
+  #TabsToolbar.webext-collapsed {
+    min-height: 22px;
+  }
+}
+
+@media (-moz-windows-classic) {
+  #TabsToolbar.webext-collapsed {
+    min-height: 16px;
+  }
+}
+
+#toolbar-menubar:not([inactive]) + #TabsToolbar {
+  min-height: 0 !important;
+}
+
 #nav-bar {
   border-top: 1px solid var(--tabs-border) !important;
 }
 @media (-moz-windows-compositor: 0) {
   #TabsToolbar[collapsed="true"] + #nav-bar {
     border-top-style: none !important;
   }
 }