Bug 1458013 - Add ability to select a range of tabs using Shift. r?jaws draft multiselect_Shift_good
authorlayely <ablayelyfondou@gmail.com>
Thu, 17 May 2018 02:38:14 +0000
branchmultiselect_Shift_good
changeset 796583 29a233e40490d217ffdd78c1d14ad9a1218db79e
parent 796038 d5b463283f3983c0e7c9d78cb8559f89b52e1cd3
push id110288
push userbmo:ablayelyfondou@gmail.com
push dateThu, 17 May 2018 20:46:14 +0000
reviewersjaws
bugs1458013
milestone62.0a1
Bug 1458013 - Add ability to select a range of tabs using Shift. r?jaws MozReview-Commit-ID: DQxhkTEyRyq
browser/base/content/tabbrowser.js
browser/base/content/tabbrowser.xml
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js
browser/base/content/test/tabs/head.js
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -131,16 +131,18 @@ window._gBrowser = {
     "userTypedValue", "userTypedClear",
     "didStartLoadSinceLastUserTyping", "audioMuted"
   ],
 
   _removingTabs: [],
 
   _multiSelectedTabsMap: new WeakMap(),
 
+  _lastMultiSelectedTabRef: null,
+
   /**
    * Tab close requests are ignored if the window is closing anyway,
    * e.g. when holding Ctrl+W.
    */
   _windowIsClosing: false,
 
   /**
    * This defines a proxy which allows us to access browsers by
@@ -3612,16 +3614,42 @@ window._gBrowser = {
     if (aTab.multiselected) {
       return;
     }
 
     aTab.setAttribute("multiselected", "true");
     this._multiSelectedTabsMap.set(aTab, null);
   },
 
+  /**
+   * Adds two given tabs and all tabs between them into the (multi) selected tabs collection
+   */
+  addRangeToMultiSelectedTabs(aTab1, aTab2) {
+    // Let's avoid going through all the heavy process below when the same
+    // tab is given as params.
+    if (aTab1 == aTab2) {
+      this.addToMultiSelectedTabs(aTab1);
+      return;
+    }
+
+    const tabs = [...this.tabs];
+    const indexOfTab1 = tabs.indexOf(aTab1);
+    const indexOfTab2 = tabs.indexOf(aTab2);
+
+    const [lowerIndex, higherIndex] = indexOfTab1 < indexOfTab2 ?
+      [indexOfTab1, indexOfTab2] : [indexOfTab2, indexOfTab1];
+
+    for (let i = lowerIndex; i <= higherIndex; i++) {
+      let tab = tabs[i];
+      if (!tab.hidden) {
+        this.addToMultiSelectedTabs(tab);
+      }
+    }
+  },
+
   removeFromMultiSelectedTabs(aTab) {
     if (!aTab.multiselected) {
       return;
     }
     aTab.removeAttribute("multiselected");
     this._multiSelectedTabsMap.delete(aTab);
   },
 
@@ -3630,22 +3658,34 @@ window._gBrowser = {
     for (let tab of selectedTabs) {
       if (tab.isConnected && tab.multiselected) {
         tab.removeAttribute("multiselected");
       }
     }
     this._multiSelectedTabsMap = new WeakMap();
   },
 
-  multiSelectedTabsCount() {
+  get multiSelectedTabsCount() {
     return ChromeUtils.nondeterministicGetWeakMapKeys(this._multiSelectedTabsMap)
       .filter(tab => tab.isConnected)
       .length;
   },
 
+  get lastMultiSelectedTab() {
+    let tab = this._lastMultiSelectedTabRef ? this._lastMultiSelectedTabRef.get() : null;
+    if (tab && tab.isConnected && this._multiSelectedTabsMap.has(tab)) {
+      return tab;
+    }
+    return gBrowser.selectedTab;
+  },
+
+  set lastMultiSelectedTab(aTab) {
+    this._lastMultiSelectedTabRef = Cu.getWeakReference(aTab);
+  },
+
   activateBrowserForPrintPreview(aBrowser) {
     this._printPreviewBrowsers.add(aBrowser);
     if (this._switcher) {
       this._switcher.activateBrowserForPrintPreview(aBrowser);
     }
     aBrowser.docShellIsActive = true;
   },
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1962,40 +1962,48 @@
         }
 
         if (this.selected) {
           this.style.MozUserFocus = "ignore";
         } else {
           // When browser.tabs.multiselect config is set to false,
           // then we ignore the state of multi-selection keys (Ctrl/Cmd).
           const tabSelectionToggled = Services.prefs.getBoolPref("browser.tabs.multiselect") &&
-            event.getModifierState("Accel");
+            (event.getModifierState("Accel") || event.shiftKey);
 
           if (this.mOverCloseButton || this._overPlayingIcon || tabSelectionToggled) {
             // Prevent tabbox.xml from selecting the tab.
             event.stopPropagation();
           }
         }
       ]]>
       </handler>
       <handler event="mouseup">
         this.style.MozUserFocus = "";
       </handler>
 
       <handler event="click" button="0"><![CDATA[
         if (Services.prefs.getBoolPref("browser.tabs.multiselect")) {
-          const tabSelectionToggled = event.getModifierState("Accel");
-          if (tabSelectionToggled) {
+          if (event.shiftKey) {
+            const lastSelectedTab = gBrowser.lastMultiSelectedTab || gBrowser.selectedTab;
+            gBrowser.addRangeToMultiSelectedTabs(lastSelectedTab, this);
+            gBrowser.lastMultiSelectedTab = this;
+            return;
+          }
+          if (event.getModifierState("Accel")) {
+            // Ctrl (Cmd for mac) key is pressed
             if (this.multiselected) {
               gBrowser.removeFromMultiSelectedTabs(this);
             } else {
               gBrowser.addToMultiSelectedTabs(this);
+              gBrowser.lastMultiSelectedTab = this;
             }
             return;
-          } else if (gBrowser.multiSelectedTabsCount() > 0) {
+          }
+          if (gBrowser.multiSelectedTabsCount > 0) {
             // Tabs were previously multi-selected and user clicks on a tab
             // without holding Ctrl/Cmd Key
             gBrowser.clearMultiSelectedTabs();
           }
         }
 
         if (this._overPlayingIcon) {
           this.toggleMuteAudio();
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -38,8 +38,9 @@ skip-if = (debug && os == 'mac') || (deb
 [browser_tabReorder_overflow.js]
 [browser_tabswitch_updatecommands.js]
 [browser_viewsource_of_data_URI_in_file_process.js]
 [browser_visibleTabs_bookmarkAllTabs.js]
 [browser_visibleTabs_contextMenu.js]
 [browser_open_newtab_start_observer_notification.js]
 [browser_bug_1387976_restore_lazy_tab_browser_muted_state.js]
 [browser_multiselect_tabs_using_Ctrl.js]
+[browser_multiselect_tabs_using_Shift.js]
--- a/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Ctrl.js
@@ -1,26 +1,10 @@
 const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
 
-function triggerClickOn(target, options) {
-  let promise = BrowserTestUtils.waitForEvent(target, "click");
-  if (AppConstants.platform == "macosx") {
-    options = { metaKey: options.ctrlKey };
-  }
-  EventUtils.synthesizeMouseAtCenter(target, options);
-  return promise;
-}
-
-async function addTab() {
-  const tab = BrowserTestUtils.addTab(gBrowser, "http://mochi.test:8888/", {skipAnimation: true});
-  const browser = gBrowser.getBrowserForTab(tab);
-  await BrowserTestUtils.browserLoaded(browser);
-  return tab;
-}
-
 add_task(async function clickWithoutPrefSet() {
   let tab = await addTab();
   let mSelectedTabs = gBrowser._multiSelectedTabsMap;
 
   isnot(gBrowser.selectedTab, tab, "Tab doesn't have focus");
 
   // We make sure that the tab-switch is completely done before executing checks
   await BrowserTestUtils.switchTab(gBrowser, () => {
@@ -50,46 +34,46 @@ add_task(async function clickWithPrefSet
   const tab = await addTab();
 
   await triggerClickOn(tab, { ctrlKey: true });
   ok(tab.multiselected && mSelectedTabs.has(tab), "Tab should be (multi) selected after click");
   isnot(gBrowser.selectedTab, tab, "Multi-selected tab is not focused");
   is(gBrowser.selectedTab, initialFocusedTab, "Focused tab doesn't change");
 
   await triggerClickOn(tab, { ctrlKey: true });
-  ok(!tab.multiselected && !mSelectedTabs.has(tab), "Tab is not selected anymore");
+  ok(!tab.multiselected && !mSelectedTabs.has(tab), "Tab is not (multi) selected anymore");
   is(gBrowser.selectedTab, initialFocusedTab, "Focused tab still doesn't change");
 
   BrowserTestUtils.removeTab(tab);
 });
 
 add_task(async function clearSelection() {
   await SpecialPowers.pushPrefEnv({
     set: [
       [PREF_MULTISELECT_TABS, true]
     ]
   });
 
   const tab1 = await addTab();
   const tab2 = await addTab();
   const tab3 = await addTab();
 
-  info("We select tab1 and tab2 with ctrl key down");
+  info("We multi-select tab1 and tab2 with ctrl key down");
   await triggerClickOn(tab1, { ctrlKey: true });
   await triggerClickOn(tab2, { ctrlKey: true });
 
   ok(tab1.multiselected && gBrowser._multiSelectedTabsMap.has(tab1), "Tab1 is (multi) selected");
   ok(tab2.multiselected && gBrowser._multiSelectedTabsMap.has(tab2), "Tab2 is (multi) selected");
-  is(gBrowser.multiSelectedTabsCount(), 2, "Two tabs selected");
+  is(gBrowser.multiSelectedTabsCount, 2, "Two tabs (multi) selected");
   isnot(tab3, gBrowser.selectedTab, "Tab3 doesn't have focus");
 
   info("We select tab3 with Ctrl key up");
   await triggerClickOn(tab3, { ctrlKey: false });
 
-  ok(!tab1.multiselected, "Tab1 is unselected");
-  ok(!tab2.multiselected, "Tab2 is unselected");
-  is(gBrowser.multiSelectedTabsCount(), 0, "Selection is cleared");
+  ok(!tab1.multiselected, "Tab1 is not (multi) selected");
+  ok(!tab2.multiselected, "Tab2 is not (multi) selected");
+  is(gBrowser.multiSelectedTabsCount, 0, "Multi-selection is cleared");
   is(tab3, gBrowser.selectedTab, "Tab3 has focus");
 
   BrowserTestUtils.removeTab(tab1);
   BrowserTestUtils.removeTab(tab2);
   BrowserTestUtils.removeTab(tab3);
 });
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_using_Shift.js
@@ -0,0 +1,125 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+
+add_task(async function prefNotSet() {
+    let tab1 = await addTab();
+    let tab2 = await addTab();
+    let tab3 = await addTab();
+
+    let mSelectedTabs = gBrowser._multiSelectedTabsMap;
+
+    await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+    is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+    is(gBrowser.multiSelectedTabsCount, 0, "No tab is mutli-selected");
+
+    info("Click on tab3 while holding shift key");
+    await BrowserTestUtils.switchTab(gBrowser, () => {
+        triggerClickOn(tab3, { shiftKey: true });
+    });
+
+    ok(!tab1.multiselected && !mSelectedTabs.has(tab1), "Tab1 is not multi-selected");
+    ok(!tab2.multiselected && !mSelectedTabs.has(tab2), "Tab2 is not multi-selected");
+    ok(!tab3.multiselected && !mSelectedTabs.has(tab3), "Tab3 is not multi-selected");
+    is(gBrowser.multiSelectedTabsCount, 0, "There is still no multi-selected tab");
+    is(gBrowser.selectedTab, tab3, "Tab3 has focus now");
+
+    BrowserTestUtils.removeTab(tab1);
+    BrowserTestUtils.removeTab(tab2);
+    BrowserTestUtils.removeTab(tab3);
+});
+
+add_task(async function setPref() {
+    await SpecialPowers.pushPrefEnv({
+        set: [
+            [PREF_MULTISELECT_TABS, true]
+        ]
+    });
+});
+
+add_task(async function noItemsInTheCollectionBeforeShiftClicking() {
+    let tab1 = await addTab();
+    let tab2 = await addTab();
+    let tab3 = await addTab();
+    let tab4 = await addTab();
+    let tab5 = await addTab();
+    let mSelectedTabs = gBrowser._multiSelectedTabsMap;
+
+    await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+    is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+    is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+    gBrowser.hideTab(tab3);
+    ok(tab3.hidden, "Tab3 is hidden");
+
+    info("Click on tab4 while holding shift key");
+    await triggerClickOn(tab4, { shiftKey: true });
+
+    ok(tab1.multiselected && mSelectedTabs.has(tab1), "Tab1 is multi-selected");
+    ok(tab2.multiselected && mSelectedTabs.has(tab2), "Tab2 is multi-selected");
+    ok(!tab3.multiselected && !mSelectedTabs.has(tab3), "Hidden tab3 is not multi-selected");
+    ok(tab4.multiselected && mSelectedTabs.has(tab4), "Tab4 is multi-selected");
+    ok(!tab5.multiselected && !mSelectedTabs.has(tab5), "Tab5 is not multi-selected");
+    is(gBrowser.multiSelectedTabsCount, 3, "three multi-selected tabs");
+    is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+
+    BrowserTestUtils.removeTab(tab1);
+    BrowserTestUtils.removeTab(tab2);
+    BrowserTestUtils.removeTab(tab3);
+    BrowserTestUtils.removeTab(tab4);
+    BrowserTestUtils.removeTab(tab5);
+});
+
+add_task(async function itemsInTheCollectionBeforeShiftClicking() {
+    let tab1 = await addTab();
+    let tab2 = await addTab();
+    let tab3 = await addTab();
+    let tab4 = await addTab();
+    let tab5 = await addTab();
+
+    let mSelectedTabs = gBrowser._multiSelectedTabsMap;
+
+    await BrowserTestUtils.switchTab(gBrowser, () => triggerClickOn(tab1, {}));
+
+    is(gBrowser.selectedTab, tab1, "Tab1 has focus now");
+    is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+    await triggerClickOn(tab3, { ctrlKey: true });
+    is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+    is(gBrowser.multiSelectedTabsCount, 1, "One tab is multi-selected");
+    ok(tab3.multiselected && mSelectedTabs.has(tab3), "Tab3 is multi-selected");
+
+    info("Click on tab5 while holding Shift key");
+    await triggerClickOn(tab5, { shiftKey: true });
+
+    is(gBrowser.selectedTab, tab1, "Tab1 still has focus");
+    ok(!tab1.multiselected && !mSelectedTabs.has(tab1), "Tab1 is not multi-selected");
+    ok(!tab2.multiselected && !mSelectedTabs.has(tab2), "Tab2 is not multi-selected ");
+    ok(tab3.multiselected && mSelectedTabs.has(tab3), "Tab3 is multi-selected");
+    ok(tab4.multiselected && mSelectedTabs.has(tab4), "Tab4 is multi-selected");
+    ok(tab5.multiselected && mSelectedTabs.has(tab5), "Tab5 is multi-selected");
+    is(gBrowser.multiSelectedTabsCount, 3, "Three tabs are multi-selected");
+
+    BrowserTestUtils.removeTab(tab1);
+    BrowserTestUtils.removeTab(tab2);
+    BrowserTestUtils.removeTab(tab3);
+    BrowserTestUtils.removeTab(tab4);
+    BrowserTestUtils.removeTab(tab5);
+});
+
+add_task(async function shiftHasHigherPrecOverCtrl() {
+    const tab1 = await addTab();
+    const tab2 = await addTab();
+
+    await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+    is(gBrowser.multiSelectedTabsCount, 0, "No tab is multi-selected");
+
+    info("Click on tab2 with both Ctrl/Cmd and Shift down");
+    await triggerClickOn(tab2, { ctrlKey: true, shiftKey: true });
+
+    is(gBrowser.multiSelectedTabsCount, 2, "Both tab1 and tab2 are multi-selected");
+
+    BrowserTestUtils.removeTab(tab1);
+    BrowserTestUtils.removeTab(tab2);
+});
--- a/browser/base/content/test/tabs/head.js
+++ b/browser/base/content/test/tabs/head.js
@@ -4,8 +4,27 @@ function updateTabContextMenu(tab) {
     tab = gBrowser.selectedTab;
   var evt = new Event("");
   tab.dispatchEvent(evt);
   menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
   is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
   menu.hidePopup();
 }
 
+function triggerClickOn(target, options) {
+  let promise = BrowserTestUtils.waitForEvent(target, "click");
+  if (AppConstants.platform == "macosx") {
+      options = {
+          metaKey: options.ctrlKey,
+          shiftKey: options.shiftKey
+      };
+  }
+  EventUtils.synthesizeMouseAtCenter(target, options);
+  return promise;
+}
+
+async function addTab() {
+  const tab = BrowserTestUtils.addTab(gBrowser,
+      "http://mochi.test:8888/", { skipAnimation: true });
+  const browser = gBrowser.getBrowserForTab(tab);
+  await BrowserTestUtils.browserLoaded(browser);
+  return tab;
+}