Bug 1419195: Show items from WebExtensions in Places Library context menu
MozReview-Commit-ID: BovEw3J8sSW
--- 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"],
},