Bug 1419195: Show items from WebExtensions in Places Library context menu draft
authorPeter Simonyi <pts@petersimonyi.ca>
Mon, 16 Jul 2018 08:45:17 -0400
changeset 820280 af017452f04a9f7b0a8bcf5c4c06c51cc07c6ba2
parent 820279 fe49a91e61b20c5a5d991c448af1168dfbc1b5bf
push id116783
push userbmo:pts+bmo@petersimonyi.ca
push dateThu, 19 Jul 2018 11:47:49 +0000
bugs1419195
milestone63.0a1
Bug 1419195: Show items from WebExtensions in Places Library context menu MozReview-Commit-ID: BovEw3J8sSW
browser/components/extensions/parent/ext-menus.js
browser/components/extensions/test/browser/browser_ext_contextMenus.js
--- a/browser/components/extensions/parent/ext-menus.js
+++ b/browser/components/extensions/parent/ext-menus.js
@@ -1,14 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/Services.jsm");
 
+ChromeUtils.defineModuleGetter(this, "Bookmarks",
+                               "resource://gre/modules/Bookmarks.jsm");
+
 var {
   DefaultMap,
   ExtensionError,
 } = ExtensionUtils;
 
 ChromeUtils.import("resource://gre/modules/ExtensionParent.jsm");
 
 var {
@@ -712,35 +715,95 @@ MenuItem.prototype = {
         return false;
       }
     }
 
     return true;
   },
 };
 
+// windowTracker only looks as browser windows, but we're also interested in
+// the Library window.  Helper fo menuTracker below.
+const libraryTracker = {
+  libraryWindowType: "Places:Organizer",
+
+  isLibraryWindow(window) {
+    let winType = window.document.documentElement.getAttribute("windowtype");
+    return winType === this.libraryWindowType;
+  },
+
+  init(listener) {
+    this._listener = listener;
+    Services.ww.registerNotification(this);
+
+    // See WindowTrackerBase#*browserWindows in ext-tabs-base.js for why we
+    // can't use the enumerator's windowtype filter.
+    let e = Services.wm.getEnumerator("");
+    while (e.hasMoreElements()) {
+      let window = e.getNext();
+      if (window.document.readyState === "complete") {
+        if (this.isLibraryWindow(window)) {
+          this.notify(window);
+        }
+      } else {
+        window.addEventListener("load", this, {once: true});
+      }
+    }
+  },
+
+  uninit() {
+    Services.ww.unregisterNotification(this);
+  },
+
+  // Gets notifications from Services.ww.registerNotification.
+  // Defer actually doing anything until the window's loaded, though.
+  observe(window, topic) {
+    if (topic === "domwindowopened") {
+      window.addEventListener("load", this, {once: true});
+    }
+  },
+
+  // Gets the load event for new windows(registered in observe()).
+  handleEvent(event) {
+    let window = event.target.defaultView;
+    if (this.isLibraryWindow(window)) {
+      this.notify(window);
+    }
+  },
+
+  notify(window) {
+    try {
+      this._listener.call(null, window);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+  },
+};
+
 // While any extensions are active, this Tracker registers to observe/listen
 // for menu events from both Tools and context menus, both content and chrome.
 const menuTracker = {
   menuIds: ["placesContext", "menu_ToolsPopup", "tabContextMenu"],
 
   register() {
     Services.obs.addObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
       this.onWindowOpen(window);
     }
     windowTracker.addOpenListener(this.onWindowOpen);
+    libraryTracker.init(this.onLibraryOpen);
   },
 
   unregister() {
     Services.obs.removeObserver(this, "on-build-contextmenu");
     for (const window of windowTracker.browserWindows()) {
       this.cleanupWindow(window);
     }
     windowTracker.removeOpenListener(this.onWindowOpen);
+    libraryTracker.uninit();
   },
 
   observe(subject, topic, data) {
     subject = subject.wrappedJSObject;
     gMenuBuilder.build(subject);
   },
 
   onWindowOpen(window) {
@@ -780,21 +843,27 @@ const menuTracker = {
     const URL = window.document.getElementById("viewBookmarksSidebar")
                                .getAttribute("sidebarurl");
     if (window.sidebar.location.href === URL) {
       const menu = window.sidebar.document.getElementById("placesContext");
       menu.addEventListener("popupshowing", menuTracker);
     }
   },
 
+  onLibraryOpen(window) {
+    const menu = window.document.getElementById("placesContext");
+    menu.addEventListener("popupshowing", menuTracker);
+  },
+
   handleEvent(event) {
     const menu = event.target;
 
-    if (event.view === event.view.parent.sidebar) {
-      return menuTracker.handleSidebarCtxMenu(event);
+    if (event.view.PlacesOrganizer ||
+        event.view === event.view.parent.sidebar) {
+      return menuTracker.handlePlacesTreeCtxMenu(event);
     }
 
     if (menu.id === "placesContext") {
       const trigger = menu.triggerNode;
       if (!trigger._placesNode) {
         return;
       }
 
@@ -812,22 +881,26 @@ const menuTracker = {
     if (menu.id === "tabContextMenu") {
       const trigger = menu.triggerNode;
       const tab = trigger.localName === "tab" ? trigger : tabTracker.activeTab;
       const pageUrl = tab.linkedBrowser.currentURI.spec;
       gMenuBuilder.build({menu, tab, pageUrl, onTab: true});
     }
   },
 
-  handleSidebarCtxMenu(event) {
+  handlePlacesTreeCtxMenu(event) {
     const menu = event.target;
     const tree = menu.triggerNode.parentElement;
     const cell = tree.boxObject.getCellAt(event.x, event.y);
     const node = tree.view.nodeForTreeIndex(cell.row);
 
+    if (!node.bookmarkGuid || Bookmarks.isVirtualRootItem(node.bookmarkGuid)) {
+      return;
+    }
+
     gMenuBuilder.build({
       menu,
       bookmarkId: node.bookmarkGuid,
       onBookmark: true,
     });
   },
 };
 
--- a/browser/components/extensions/test/browser/browser_ext_contextMenus.js
+++ b/browser/components/extensions/test/browser/browser_ext_contextMenus.js
@@ -1,16 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/browser/components/places/tests/browser/head.js",
   this);
-/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell */
+/* globals withSidebarTree, synthesizeClickOnSelectedTreeCell,
+ * promiseLibrary, promiseLibraryClosed
+ */
 
 const PAGE = "http://mochi.test:8888/browser/browser/components/extensions/test/browser/context.html";
 
 add_task(async function() {
   let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
 
   gBrowser.selectedTab = tab1;
 
@@ -585,16 +587,77 @@ add_task(async function test_bookmark_si
 
     let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
     closeChromeContextMenu("placesContext", menuItem, window.sidebar);
     await extension.awaitMessage("test-finish");
     await extension.unload();
   });
 });
 
+function bookmarkFolderContextMenuExtension() {
+  return ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["contextMenus", "bookmarks"],
+    },
+    async background() {
+      const title = "Example";
+      let newBookmark = await browser.bookmarks.create({
+        title,
+        parentId: "toolbar_____",
+      });
+      await browser.contextMenus.create({
+        title: "Get bookmark",
+        contexts: ["bookmark"],
+      });
+      browser.test.sendMessage("bookmark-created", newBookmark.id);
+      browser.contextMenus.onClicked.addListener(async (info) => {
+        browser.test.assertEq(newBookmark.id, info.bookmarkId, "Bookmark ID matches");
+
+        let [bookmark] = await browser.bookmarks.get(info.bookmarkId);
+        browser.test.assertEq(title, bookmark.title, "Bookmark title matches");
+        browser.test.assertFalse(info.hasOwnProperty("pageUrl"), "Context menu does not expose pageUrl");
+        await browser.bookmarks.remove(info.bookmarkId);
+        browser.test.sendMessage("test-finish");
+      });
+    },
+  });
+}
+
+add_task(async function test_organizer_contextmenu() {
+  let library = await promiseLibrary("BookmarksToolbar");
+
+  let menu = library.document.getElementById("placesContext");
+  let mainTree = library.document.getElementById("placeContent");
+  let leftTree = library.document.getElementById("placesList");
+
+  let tests = [
+    [mainTree, bookmarkContextMenuExtension],
+    [mainTree, bookmarkFolderContextMenuExtension],
+    [leftTree, bookmarkFolderContextMenuExtension],
+  ];
+
+  for (let [tree, makeExtension] of tests) {
+    let extension = makeExtension();
+    await extension.startup();
+    let bookmarkGuid = await extension.awaitMessage("bookmark-created");
+
+    tree.selectItems([bookmarkGuid]);
+    let shown = BrowserTestUtils.waitForEvent(menu, "popupshown");
+    synthesizeClickOnSelectedTreeCell(tree, {type: "contextmenu"});
+    await shown;
+
+    let menuItem = menu.getElementsByAttribute("label", "Get bookmark")[0];
+    closeChromeContextMenu("placesContext", menuItem, library);
+    await extension.awaitMessage("test-finish");
+    await extension.unload();
+  }
+
+  await promiseLibraryClosed(library);
+});
+
 add_task(async function test_bookmark_context_requires_permission() {
   const bookmarksToolbar = document.getElementById("PersonalToolbar");
   setToolbarVisibility(bookmarksToolbar, true);
 
   const extension = ExtensionTestUtils.loadExtension({
     manifest: {
       permissions: ["contextMenus"],
     },