Bug 1288247 - Open synced tabs in a new tab on middle click. r?markh draft
authorEdouard Oger <eoger@fastmail.com>
Fri, 19 Aug 2016 17:10:45 -0700
changeset 405057 09978d07bb82b7dd93d917e9af7ce0f32020b979
parent 404476 052656fc513c05da969590ac5934abd67271a897
child 529340 fc5178573d41a5cbf1d2e53a39ca3ca6b4597746
push id27377
push userbmo:eoger@fastmail.com
push dateWed, 24 Aug 2016 16:53:27 +0000
reviewersmarkh
bugs1288247
milestone51.0a1
Bug 1288247 - Open synced tabs in a new tab on middle click. r?markh MozReview-Commit-ID: LTE5NolY8V3
browser/components/places/PlacesUIUtils.jsm
browser/components/syncedtabs/TabListComponent.js
browser/components/syncedtabs/TabListView.js
browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -879,18 +879,17 @@ this.PlacesUIUtils = {
 
     return itemId == this.leftPaneFolderId ||
            itemId == this.allBookmarksFolderId;
   },
 
   /**
    * Gives the user a chance to cancel loading lots of tabs at once
    */
-  _confirmOpenInTabs:
-  function PUIU__confirmOpenInTabs(numTabsToOpen, aWindow) {
+  confirmOpenInTabs(numTabsToOpen, aWindow) {
     const WARN_ON_OPEN_PREF = "browser.tabs.warnOnOpen";
     var reallyOpen = true;
 
     if (Services.prefs.getBoolPref(WARN_ON_OPEN_PREF)) {
       if (numTabsToOpen >= Services.prefs.getIntPref("browser.tabs.maxOpenBeforeWarn")) {
         // default to true: if it were false, we wouldn't get this far
         var warnOnOpen = { value: true };
 
@@ -983,28 +982,28 @@ this.PlacesUIUtils = {
       .then(aLivemark => {
         urlsToOpen = [];
 
         let nodes = aLivemark.getNodesForContainer(aNode);
         for (let node of nodes) {
           urlsToOpen.push({uri: node.uri, isBookmark: false});
         }
 
-        if (this._confirmOpenInTabs(urlsToOpen.length, window)) {
+        if (this.confirmOpenInTabs(urlsToOpen.length, window)) {
           this._openTabset(urlsToOpen, aEvent, window);
         }
       }, Cu.reportError);
   },
 
   openContainerNodeInTabs:
   function PUIU_openContainerInTabs(aNode, aEvent, aView) {
     let window = aView.ownerWindow;
 
     let urlsToOpen = PlacesUtils.getURLsForContainerNode(aNode);
-    if (this._confirmOpenInTabs(urlsToOpen.length, window)) {
+    if (this.confirmOpenInTabs(urlsToOpen.length, window)) {
       this._openTabset(urlsToOpen, aEvent, window);
     }
   },
 
   openURINodesInTabs: function PUIU_openURINodesInTabs(aNodes, aEvent, aView) {
     let window = aView.ownerWindow;
 
     let urlsToOpen = [];
--- a/browser/components/syncedtabs/TabListComponent.js
+++ b/browser/components/syncedtabs/TabListComponent.js
@@ -8,16 +8,18 @@ const {classes: Cc, interfaces: Ci, util
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 let log = Cu.import("resource://gre/modules/Log.jsm", {})
             .Log.repository.getLogger("Sync.RemoteTabs");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
   "resource:///modules/BrowserUITelemetry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils",
+  "resource:///modules/PlacesUIUtils.jsm");
 
 this.EXPORTED_SYMBOLS = [
   "TabListComponent"
 ];
 
 /**
  * TabListComponent
  *
@@ -42,16 +44,17 @@ TabListComponent.prototype = {
   },
 
   init() {
     log.debug("Initializing TabListComponent");
 
     this._view = new this._View(this._window, {
       onSelectRow: (...args) => this.onSelectRow(...args),
       onOpenTab: (...args) => this.onOpenTab(...args),
+      onOpenTabs: (...args) => this.onOpenTabs(...args),
       onMoveSelectionDown: (...args) => this.onMoveSelectionDown(...args),
       onMoveSelectionUp: (...args) => this.onMoveSelectionUp(...args),
       onToggleBranch: (...args) => this.onToggleBranch(...args),
       onBookmarkTab: (...args) => this.onBookmarkTab(...args),
       onCopyTabLocation: (...args) => this.onCopyTabLocation(...args),
       onSyncRefresh: (...args) => this.onSyncRefresh(...args),
       onFilter: (...args) => this.onFilter(...args),
       onClearFilter: (...args) => this.onClearFilter(...args),
@@ -108,16 +111,31 @@ TabListComponent.prototype = {
       .catch(Cu.reportError);
   },
 
   onOpenTab(url, where, params) {
     this._window.openUILinkIn(url, where, params);
     BrowserUITelemetry.countSyncedTabEvent("open", "sidebar");
   },
 
+  onOpenTabs(urls, where, params) {
+    if (!PlacesUIUtils.confirmOpenInTabs(urls.length, this._window)) {
+      return;
+    }
+    if (where == "window") {
+      this._window.openDialog(this._window.getBrowserURL(), "_blank",
+                              "chrome,dialog=no,all", urls.join("|"));
+    } else {
+      for (let url of urls) {
+        this._window.openUILinkIn(url, where, params);
+      }
+    }
+    BrowserUITelemetry.countSyncedTabEvent("openmultiple", "sidebar");
+  },
+
   onCopyTabLocation(url) {
     this._clipboardHelper.copyString(url);
   },
 
   onSyncRefresh() {
     this._SyncedTabs.syncTabs(true);
   }
 };
--- a/browser/components/syncedtabs/TabListView.js
+++ b/browser/components/syncedtabs/TabListView.js
@@ -177,16 +177,17 @@ TabListView.prototype = {
     this.tabsFilter.addEventListener("blur", this.onFilterBlur.bind(this));
     this.clearFilter.addEventListener("click", this.onClearFilter.bind(this));
     this.searchIcon.addEventListener("click", this.onFilterFocus.bind(this));
   },
 
   // These listeners have to be re-created every time since we re-create the list
   _attachListListeners() {
     this.list.addEventListener("click", this.onClick.bind(this));
+    this.list.addEventListener("mouseup", this.onMouseUp.bind(this));
     this.list.addEventListener("keydown", this.onKeyDown.bind(this));
   },
 
   _updateSearchBox(state) {
     if (state.filter) {
       this.searchBox.classList.add("filtered");
     } else {
       this.searchBox.classList.remove("filtered");
@@ -255,30 +256,47 @@ TabListView.prototype = {
     itemNode.querySelector(".item-title").textContent = item.title;
 
     if (item.icon) {
       let icon = itemNode.querySelector(".item-icon-container");
       icon.style.backgroundImage = "url(" + item.icon + ")";
     }
   },
 
+  onMouseUp(event) {
+    if (event.which == 2) { // Middle click
+      this.onClick(event);
+    }
+  },
+
   onClick(event) {
     let itemNode = this._findParentItemNode(event.target);
     if (!itemNode) {
       return;
     }
 
     if (itemNode.classList.contains("tab")) {
       let url = itemNode.dataset.url;
       if (url) {
         this.onOpenSelected(url, event);
       }
     }
 
-    if (event.target.classList.contains("item-twisty-container")) {
+    // Middle click on a client
+    if (itemNode.classList.contains("client")) {
+      let where = getChromeWindow(this._window).whereToOpenLink(event);
+      if (where != "current") {
+        const tabs = itemNode.querySelector(".item-tabs-list").childNodes;
+        const urls = [...tabs].map(tab => tab.dataset.url);
+        this.props.onOpenTabs(urls, where, {});
+      }
+    }
+
+    if (event.target.classList.contains("item-twisty-container")
+        && event.which != 2) {
       this.props.onToggleBranch(itemNode.dataset.id);
       return;
     }
 
     let position = this._getSelectionPosition(itemNode);
     this.props.onSelectRow(position);
   },
 
--- a/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
+++ b/browser/components/syncedtabs/test/xpcshell/test_TabListComponent.js
@@ -3,16 +3,17 @@
 let { SyncedTabs } = Cu.import("resource://services-sync/SyncedTabs.jsm", {});
 let { TabListComponent } = Cu.import("resource:///modules/syncedtabs/TabListComponent.js", {});
 let { SyncedTabsListStore } = Cu.import("resource:///modules/syncedtabs/SyncedTabsListStore.js", {});
 let { View } = Cu.import("resource:///modules/syncedtabs/TabListView.js", {});
 
 const ACTION_METHODS = [
   "onSelectRow",
   "onOpenTab",
+  "onOpenTabs",
   "onMoveSelectionDown",
   "onMoveSelectionUp",
   "onToggleBranch",
   "onBookmarkTab",
   "onSyncRefresh",
   "onFilter",
   "onClearFilter",
   "onFilterFocus",
@@ -72,16 +73,18 @@ add_task(function* testActions() {
   };
   let windowMock = {
     top: {
       PlacesCommandHook: {
         bookmarkLink() { return Promise.resolve(); }
       },
       PlacesUtils: { bookmarksMenuFolderId: "id" }
     },
+    getBrowserURL() {},
+    openDialog() {},
     openUILinkIn() {}
   };
   let component = new TabListComponent({
     window: windowMock, store, View: null, SyncedTabs,
     clipboardHelper: clipboardHelperMock});
 
   sinon.stub(store, "getData");
   component.onFilter("query");
@@ -119,16 +122,23 @@ add_task(function* testActions() {
   component.onBookmarkTab("uri", "title");
   Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][1], "uri");
   Assert.equal(windowMock.top.PlacesCommandHook.bookmarkLink.args[0][2], "title");
 
   sinon.spy(windowMock, "openUILinkIn");
   component.onOpenTab("uri", "where", "params");
   Assert.ok(windowMock.openUILinkIn.calledWith("uri", "where", "params"));
 
+  component.onOpenTabs(["uri1", "uri2"], "where", "params");
+  Assert.ok(windowMock.openUILinkIn.calledWith("uri1", "where", "params"));
+  Assert.ok(windowMock.openUILinkIn.calledWith("uri2", "where", "params"));
+  sinon.spy(windowMock, "openDialog");
+  component.onOpenTabs(["uri1", "uri2"], "window", "params");
+  Assert.deepEqual(windowMock.openDialog.args[0][3], ["uri1", "uri2"].join("|"));
+
   sinon.spy(clipboardHelperMock, "copyString");
   component.onCopyTabLocation("uri");
   Assert.ok(clipboardHelperMock.copyString.calledWith("uri"));
 
   sinon.stub(SyncedTabs, "syncTabs");
   component.onSyncRefresh();
   Assert.ok(SyncedTabs.syncTabs.calledWith(true));
   SyncedTabs.syncTabs.restore();