Bug 1458049 - Implement ability to move a selection of tabs into a new window through tab context menu. r?jaws draft multiselect_move_ok
authorAbdoulaye O. Ly <ablayelyfondou@gmail.com>
Fri, 13 Jul 2018 19:30:58 +0000
branchmultiselect_move_ok
changeset 819607 72c8d2762d288417cb18218d6aa162a25e203682
parent 819486 afa310dc89beeb4b7a9564d2c89ff32906f427ad
push id116596
push userbmo:ablayelyfondou@gmail.com
push dateWed, 18 Jul 2018 07:37:30 +0000
reviewersjaws
bugs1458049
milestone63.0a1
Bug 1458049 - Implement ability to move a selection of tabs into a new window through tab context menu. r?jaws MozReview-Commit-ID: KrjavwyoF4s
browser/base/content/browser.xul
browser/base/content/tabbrowser.js
browser/base/content/test/tabs/browser.ini
browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -115,17 +115,17 @@
             accesskey="&reopenInContainer.accesskey;"
             hidden="true">
         <menupopup oncommand="TabContextMenu.reopenInContainer(event);"
                    onpopupshowing="TabContextMenu.createReopenInContainerMenu(event);"/>
       </menu>
       <menuitem id="context_openTabInWindow" label="&moveToNewWindow.label;"
                 accesskey="&moveToNewWindow.accesskey;"
                 tbattr="tabbrowser-multiple"
-                oncommand="gBrowser.replaceTabWithWindow(TabContextMenu.contextTab);"/>
+                oncommand="gBrowser.replaceTabsWithWindow(TabContextMenu.contextTab);"/>
       <menuseparator id="context_sendTabToDevice_separator" class="sync-ui-item"/>
       <menu id="context_sendTabToDevice" label="&sendTabToDevice.label;"
             class="sync-ui-item"
             accesskey="&sendTabToDevice.accesskey;">
         <menupopup id="context_sendTabToDevicePopupMenu"
                    onpopupshowing="gSync.populateSendTabToDevicesMenu(event.target, TabContextMenu.contextTab.linkedBrowser.currentURI.spec, TabContextMenu.contextTab.linkedBrowser.contentTitle);"/>
       </menu>
       <menuseparator/>
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -3407,16 +3407,70 @@ window._gBrowser = {
       aTab.style.maxWidth = ""; // ensure that fade-out transition happens
       aTab.removeAttribute("fadein");
     }
 
     // tell a new window to take the "dropped" tab
     return window.openDialog(getBrowserURL(), "_blank", options, aTab);
   },
 
+  /**
+   * Move contextTab (or selected tabs in a mutli-select context)
+   * to a new browser window, unless it is (they are) already the only tab(s)
+   * in the current window, in which case this will do nothing.
+   */
+  replaceTabsWithWindow(contextTab) {
+    let tabs;
+    if (contextTab.multiselected) {
+      tabs = this.selectedTabs;
+    } else {
+      tabs = [gBrowser.selectedTab];
+    }
+
+    if (this.tabs.length == tabs.length) {
+      return null;
+    }
+
+    if (tabs.length == 1) {
+      return this.replaceTabWithWindow(tabs[0]);
+    }
+
+    // The order of the tabs is reserved.
+    // To avoid mutliple tab-switch, the active tab is "moved" lastly, if applicable.
+    // If applicable, the active tab remains active in the new window.
+    let activeTab = gBrowser.selectedTab;
+    let inactiveTabs = tabs.filter(t => t != activeTab);
+    let activeTabNewIndex = tabs.indexOf(activeTab);
+
+
+    // Play the closing animation for all selected tabs to give
+    // immediate feedback while waiting for the new window to appear.
+    if (this.animationsEnabled) {
+      for (let tab of tabs) {
+        tab.style.maxWidth = ""; // ensure that fade-out transition happens
+        tab.removeAttribute("fadein");
+      }
+    }
+
+    let win;
+    let firstInactiveTab = inactiveTabs[0];
+    firstInactiveTab.linkedBrowser.addEventListener("EndSwapDocShells", function() {
+      for (let i = 1; i < inactiveTabs.length; i++) {
+        win.gBrowser.adoptTab(inactiveTabs[i], i);
+      }
+
+      if (activeTabNewIndex > -1) {
+        win.gBrowser.adoptTab(activeTab, activeTabNewIndex, true /* aSelectTab */);
+      }
+    }, { once: true });
+
+    win = this.replaceTabWithWindow(firstInactiveTab);
+    return win;
+  },
+
   _updateTabsAfterInsert() {
     for (let i = 0; i < this.tabs.length; i++) {
       this.tabs[i]._tPos = i;
       this.tabs[i]._selected = false;
     }
 
     // If we're in the midst of an async tab switch while calling
     // moveTabTo, we can get into a case where _visuallySelected
@@ -3678,17 +3732,17 @@ window._gBrowser = {
 
   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;
+    return tabs.sort((a, b) => a._tPos > b._tPos);
   },
 
   get multiSelectedTabsCount() {
     return ChromeUtils.nondeterministicGetWeakSetKeys(this._multiSelectedTabsSet)
       .filter(tab => tab.isConnected && !tab.closing)
       .length;
   },
 
--- a/browser/base/content/test/tabs/browser.ini
+++ b/browser/base/content/test/tabs/browser.ini
@@ -19,16 +19,17 @@ skip-if = (verify && debug && (os == 'li
 support-files =
   test_bug1358314.html
 [browser_isLocalAboutURI.js]
 [browser_multiselect_tabs_active_tab_selected_by_default.js]
 [browser_multiselect_tabs_bookmark.js]
 [browser_multiselect_tabs_close_other_tabs.js]
 [browser_multiselect_tabs_close_using_shortcuts.js]
 [browser_multiselect_tabs_close.js]
+[browser_multiselect_tabs_move_to_new_window_contextmenu.js]
 [browser_multiselect_tabs_mute_unmute.js]
 [browser_multiselect_tabs_pin_unpin.js]
 [browser_multiselect_tabs_positional_attrs.js]
 [browser_multiselect_tabs_reload.js]
 [browser_multiselect_tabs_using_Ctrl.js]
 [browser_multiselect_tabs_using_Shift.js]
 [browser_multiselect_tabs_using_selectedTabs.js]
 [browser_navigatePinnedTab.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/tabs/browser_multiselect_tabs_move_to_new_window_contextmenu.js
@@ -0,0 +1,46 @@
+const PREF_MULTISELECT_TABS = "browser.tabs.multiselect";
+
+add_task(async function setPref() {
+  await SpecialPowers.pushPrefEnv({
+    set: [[PREF_MULTISELECT_TABS, true]]
+  });
+});
+
+add_task(async function test() {
+  let tab1 = await addTab();
+  let tab2 = await addTab();
+  let tab3 = await addTab();
+  let tab4 = await addTab();
+
+  is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+
+  await BrowserTestUtils.switchTab(gBrowser, tab2);
+  await triggerClickOn(tab1, { ctrlKey: true });
+  await triggerClickOn(tab3, { ctrlKey: true });
+
+  ok(tab1.multiselected, "Tab1 is multiselected");
+  ok(tab2.multiselected, "Tab2 is multiselected");
+  ok(tab3.multiselected, "Tab3 is multiselected");
+  ok(!tab4.multiselected, "Tab4 is not multiselected");
+  is(gBrowser.multiSelectedTabsCount, 3, "Three multiselected tabs");
+
+  let newWindow = gBrowser.replaceTabsWithWindow(tab1);
+
+  // waiting for tab2 to close ensure that the newWindow is created,
+  // thus newWindow.gBrowser used in the second waitForCondition
+  // will not be undefined.
+  await TestUtils.waitForCondition(() => tab2.closing, "Wait for tab2 to close");
+  await TestUtils.waitForCondition(() => newWindow.gBrowser.visibleTabs.length == 3,
+    "Wait for all three tabs to get moved to the new window");
+
+  let gBrowser2 = newWindow.gBrowser;
+
+  is(gBrowser.multiSelectedTabsCount, 0, "Zero multiselected tabs");
+  is(gBrowser.visibleTabs.length, 2, "Two tabs now in the old window");
+  is(gBrowser2.visibleTabs.length, 3, "Three tabs in the new window");
+  is(gBrowser2.visibleTabs.indexOf(gBrowser2.selectedTab), 1,
+    "Previously active tab is still the active tab in the new window");
+
+  BrowserTestUtils.closeWindow(newWindow);
+  BrowserTestUtils.removeTab(tab4);
+});