Bug 1465170 - Implement support for the 'highlighted' API for multiselect tabs with tabs.query draft
authorOriol Brufau <oriol-bugzilla@hotmail.com>
Sun, 03 Jun 2018 02:53:59 +0200
changeset 811260 2b6fad330247effc1806fe7018b249791fd0afb8
parent 811103 1c235a552c32ba6c97e6030c497c49f72c7d48a8
push id114251
push userbmo:oriol-bugzilla@hotmail.com
push dateWed, 27 Jun 2018 10:51:07 +0000
bugs1465170
milestone63.0a1
Bug 1465170 - Implement support for the 'highlighted' API for multiselect tabs with tabs.query MozReview-Commit-ID: 6eFnxrXJXXB
browser/base/content/tabbrowser.js
browser/components/extensions/parent/ext-browser.js
browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
mobile/android/components/extensions/ext-utils.js
toolkit/components/extensions/parent/ext-tabs-base.js
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -2608,40 +2608,38 @@ window._gBrowser = {
     return tabsToEnd;
   },
 
   removeTabsToTheEndFrom(aTab) {
     if (!this.warnAboutClosingTabs(this.closingTabsEnum.TO_END, aTab))
       return;
 
     let tabs = this.getTabsToTheEndFrom(aTab);
-    this.removeCollectionOfTabs(tabs);
+    this.removeTabs(tabs);
   },
 
   removeAllTabsBut(aTab) {
     if (!this.warnAboutClosingTabs(this.closingTabsEnum.OTHER)) {
       return;
     }
 
     let tabs = this.visibleTabs.filter(tab => tab != aTab && !tab.pinned);
     this.selectedTab = aTab;
-    this.removeCollectionOfTabs(tabs);
+    this.removeTabs(tabs);
   },
 
   removeMultiSelectedTabs() {
     if (!this.warnAboutClosingTabs(this.closingTabsEnum.MULTI_SELECTED)) {
       return;
     }
 
-    let selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
-                                  .filter(tab => tab.isConnected);
-    this.removeCollectionOfTabs(selectedTabs);
+    this.removeTabs(this.selectedTabs);
   },
 
-  removeCollectionOfTabs(tabs) {
+  removeTabs(tabs) {
     let tabsWithBeforeUnload = [];
     let lastToClose;
     let aParams = {animation: true};
     for (let tab of tabs) {
       if (tab.selected)
         lastToClose = tab;
       else if (this._hasBeforeUnload(tab))
         tabsWithBeforeUnload.push(tab);
@@ -3674,28 +3672,35 @@ window._gBrowser = {
       return;
     }
     aTab.removeAttribute("multiselected");
     this.tabContainer._setPositionalAttributes();
     this._multiSelectedTabsSet.delete(aTab);
   },
 
   clearMultiSelectedTabs(updatePositionalAttributes) {
-    const selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet);
-    for (let tab of selectedTabs) {
-      if (tab.isConnected && tab.multiselected) {
-        tab.removeAttribute("multiselected");
-      }
+    for (let tab of this.selectedTabs) {
+      tab.removeAttribute("multiselected");
     }
     this._multiSelectedTabsSet = new WeakSet();
     if (updatePositionalAttributes) {
       this.tabContainer._setPositionalAttributes();
     }
   },
 
+  get selectedTabs() {
+    let {selectedTab, _multiSelectedTabsSet} = this;
+    let tabs = ChromeUtils.nondeterministicGetWeakSetKeys(_multiSelectedTabsSet)
+      .filter(tab => tab.isConnected && !tab.closing);
+    if (!_multiSelectedTabsSet.has(selectedTab)) {
+      tabs.push(selectedTab);
+    }
+    return tabs;
+  },
+
   get multiSelectedTabsCount() {
     return ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
       .filter(tab => tab.isConnected && !tab.closing)
       .length;
   },
 
   get lastMultiSelectedTab() {
     let tab = this._lastMultiSelectedTabRef ? this._lastMultiSelectedTabRef.get() : null;
@@ -3705,27 +3710,24 @@ window._gBrowser = {
     return gBrowser.selectedTab;
   },
 
   set lastMultiSelectedTab(aTab) {
     this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
   },
 
   toggleMuteAudioOnMultiSelectedTabs(aTab) {
-    const selectedTabs = ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
-                                    .filter(tab => tab.isConnected);
     let tabsToToggle;
-
     if (aTab.activeMediaBlocked) {
-      tabsToToggle = selectedTabs.filter(tab =>
+      tabsToToggle = this.selectedTabs.filter(tab =>
         tab.activeMediaBlocked || tab.linkedBrowser.audioMuted
       );
     } else {
       let tabMuted = aTab.linkedBrowser.audioMuted;
-      tabsToToggle = selectedTabs.filter(tab =>
+      tabsToToggle = this.selectedTabs.filter(tab =>
         // When a user is looking to mute selected tabs, then media-blocked tabs
         // should not be toggled. Otherwise those media-blocked tabs are going into a
         // playing and unmuted state.
         tab.linkedBrowser.audioMuted == tabMuted && !tab.activeMediaBlocked ||
         tab.activeMediaBlocked && tabMuted
       );
     }
     for (let tab of tabsToToggle) {
--- a/browser/components/extensions/parent/ext-browser.js
+++ b/browser/components/extensions/parent/ext-browser.js
@@ -933,16 +933,23 @@ class Window extends WindowBase {
 
     let {tabManager} = this.extension;
 
     for (let nativeTab of this.window.gBrowser.tabs) {
       yield tabManager.getWrapper(nativeTab);
     }
   }
 
+  * getHighlightedTabs() {
+    let {tabManager} = this.extension;
+    for (let tab of this.window.gBrowser.selectedTabs) {
+      yield tabManager.getWrapper(tab);
+    }
+  }
+
   get activeTab() {
     let {tabManager} = this.extension;
 
     // A new window is being opened and it is adopting a tab, and we do not create
     // a TabWrapper for the tab being adopted because it will go away once the tab
     // adoption has been completed (See Bug 1458918 for rationale).
     if (this.window.gBrowserInit.isAdoptingTab()) {
       return null;
--- a/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_highlight.js
@@ -18,16 +18,21 @@ add_task(async function test_highlighted
     background: async function() {
       async function testHighlighted(activeIndex, highlightedIndices) {
         let tabs = await browser.tabs.query({currentWindow: true});
         for (let {index, active, highlighted} of tabs) {
           browser.test.assertEq(index == activeIndex, active, "Check Tab.active: " + index);
           let expected = highlightedIndices.includes(index) || index == activeIndex;
           browser.test.assertEq(expected, highlighted, "Check Tab.highlighted: " + index);
         }
+        let highlightedTabs = await browser.tabs.query({currentWindow: true, highlighted: true});
+        browser.test.assertEq(
+          highlightedIndices.concat(activeIndex).sort((a, b) => a - b).join(),
+          highlightedTabs.map(tab => tab.index).sort((a, b) => a - b).join(),
+          "Check tabs.query with highlighted:true provides the expected tabs");
       }
 
       browser.test.log("Check that last tab is active, and no other is highlighted");
       await testHighlighted(2, []);
 
       browser.test.log("Highlight first and second tabs");
       await browser.tabs.highlight({tabs: [0, 1]});
       await testHighlighted(0, [1]);
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -662,16 +662,20 @@ class Window extends WindowBase {
   * getTabs() {
     let {tabManager} = this.extension;
 
     for (let nativeTab of this.window.BrowserApp.tabs) {
       yield tabManager.getWrapper(nativeTab);
     }
   }
 
+  * getHighlightedTabs() {
+    yield this.activeTab;
+  }
+
   get activeTab() {
     let {BrowserApp} = this.window;
     let {selectedTab} = BrowserApp;
 
     // If the current tab is an extension popup tab, we use the parentId to retrieve
     // and return the tab that was selected when the popup tab has been opened.
     if (selectedTab === tabTracker.extensionPopupTab) {
       selectedTab = BrowserApp.getTabForId(selectedTab.parentId);
--- a/toolkit/components/extensions/parent/ext-tabs-base.js
+++ b/toolkit/components/extensions/parent/ext-tabs-base.js
@@ -1044,16 +1044,25 @@ class WindowBase {
    *
    * @returns {Iterator<TabBase>}
    */
   getTabs() {
     throw new Error("Not implemented");
   }
 
   /**
+   * Returns an iterator of TabBase objects for each highlighted tab in this window.
+   *
+   * @returns {Iterator<TabBase>}
+   */
+  getHighlightedTabs() {
+    throw new Error("Not implemented");
+  }
+
+  /**
    * @property {TabBase} The window's currently active tab.
    */
   get activeTab() {
     throw new Error("Not implemented");
   }
 
   /**
    * Returns the window's tab at the specified index.
@@ -1838,27 +1847,31 @@ class TabManagerBase {
    *        Used to determine the current window for relevant properties.
    *
    * @returns {Iterator<TabBase>}
    */
   * query(queryInfo = null, context = null) {
     function* candidates(windowWrapper) {
       if (queryInfo) {
         let {active, highlighted, index} = queryInfo;
-        if (active === true || highlighted === true) {
+        if (active === true) {
           yield windowWrapper.activeTab;
           return;
         }
         if (index != null) {
           let tabWrapper = windowWrapper.getTabAtIndex(index);
           if (tabWrapper) {
             yield tabWrapper;
           }
           return;
         }
+        if (highlighted === true) {
+          yield* windowWrapper.getHighlightedTabs();
+          return;
+        }
       }
       yield* windowWrapper.getTabs();
     }
     let windowWrappers = this.extension.windowManager.query(queryInfo, context);
     for (let windowWrapper of windowWrappers) {
       for (let tabWrapper of candidates(windowWrapper)) {
         if (!queryInfo || tabWrapper.matches(queryInfo)) {
           yield tabWrapper;