What is the proper way to add context menu items in a restartless/e10s addon?

I’m in the process of migrating (full rewrite, more likely) an existing overlay extension to the restartless/bootstrapped model (no add-on SDK) with support for e10s; support for older version of Firefox (lower than around version 27-29) would be nice but not really mandatory so I’m trying to implement things as cleanly as possible and avoid “old hacks”.

I can’t seem to figure out how to add context menu items - none of the js core modules seem to provide support for this feature (like CustomizableUI does for toolbar buttons). Is there anything equivalent to https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/context-menu? If not, what are the steps required to implement something similar: what goes in the background, what needs to be implemented in frame-scripts, any API pointers/hooks to get me started…

I think it might depend on what your context menu item needs access to. Adding elements to the menu can be done via document.getElementById('contentAreaContextMenu') to get the menu element and then document.createElement and document.appendChild to create new items and attach them to the menu. I have a bootstrapped add-on with some contentAreaConextMenu items, but it only needed to work with the current page URL or the URL of the link clicked on (when a link was right-clicked) and it could access that information via gBrowser and gContextMenu. If your menu items need to interact with the page content more than just reading a link or the current URL, you might need to use a content script. I haven’t had to do that so far myself.

Keep in mind that you will need to track these added elements and remove them when you add-on is disabled (this is the kind of bookkeeping that the SDK takes care of for you – it’s worth considering if you’re overhauling the add-on code any way, though I am not as familiar with it myself, so perhaps there are other drawbacks compared to bootstrapped add-ons, though I think you can access everything a bootstrapped add-on can access if you use the chrome module). If you want to see what I did in my add-on, you can look here: https://github.com/willsALMANJ/Zutilo/blob/032bdcdc6a388c4212738d92f14db87d84c3433e/chrome/content/zutilo/firefoxOverlay.js#L198 . I have tested the menu items in Nightly with e10s enabled.

Thank you for the pointers Will - I managed to get the menu item displayed. A few people on IRC also helped with some links which I’m attaching here in case somebody else needs them:
https://addons.mozilla.org/en-US/firefox/files/browse/189760/file/bootstrap.js#L132
https://addons.mozilla.org/en-US/firefox/files/browse/201916/file/bootstrap.js
https://addons.mozilla.org/en-US/firefox/files/browse/201916/file/modules/watchwindows.jsm#top

The steps I followed are (again, if someone needs them):

  1. get existing browser windows using Services.wm.getEnumerator(“navigator:browser”) and inject the menu if they’re already loaded; if not yet fully loaded, listen for ‘load’ event (step3/4)
  2. set up a listener to get notified when new windows are opened using Services.wm.addListener(windowListener);
  3. for each window being loaded, wnd.addEventListener(onWndLoaded, false);
  4. inside onWndLoaded, create menu items
    *cleanup everything when window is closed

Now, the problem is how to control the display of the menu items. What if, for instance, I need to display the item only when there’s a form with password input in the current page? Do I really have to send a message to the frameScript and wait until the script replies telling me if the conditions are met before deciding to set the menu item on hidden or not?

You have access to some information without using a content script via gContextMenu. gContextMenu isn’t too well documented but here is one list of what you have access to:

The most relevant property to look at is target which should hold information about the target that has been clicked on. I just tested this out quickly without e10s and it seemed like target had all the information you would want, but you should probably test it yourself in something like Scratchpad (I used Pentadactyl). You can use something like this:

test = {}
cacm = document.getElementById('contentAreaContextMenu')
cacm.addEventListener('popupshowing', function() {test = gContextMenu.target}, false)

and then right-click somewhere and see what test looks like.

There is also something called gContextMenuContentData which I am not familiar with but found reference to here: http://stackoverflow.com/questions/24954664/how-to-get-document-popupnode-in-firefox-electrolysis-windows Apparently, it only works in e10s windows.

Thanks for the links - I knew about gContextMenu but I simply ignored it after reading https://developer.mozilla.org/en-US/Add-ons/Working_with_multiprocess_Firefox where it’s clearly stated that things like the code below shoudn’t function anymore:

gBrowser.selectedBrowser.contentDocument.body.innerHTML = "replaced by chrome code";

I’m not sure why gContextMenu would work in this case…unless CPOWs are used even though the extension is marked as multiprocess compatible (so shims should not be used); install.rdf contains the following line which should disable compatibility shims (and CPOWs):

<em:multiprocessCompatible>true</em:multiprocessCompatible>

The following code works well however (with e10s enabled):

function onPopupShow(ev) {
        let xulDocument = ev.currentTarget.ownerDocument;
        let gcm = xulDocument.defaultView.gContextMenu;
        let gcmcd = xulDocument.defaultView.gContextMenuContentData; //is it always available?

        let document = gcm.target.ownerDocument; //html document - can check page content???
        document.body.style.border = "10px solid red";
    }

    var ctxMenu = wnd.document.getElementById("contentAreaContextMenu"); //called from windows watcher
    ctxMenu.addEventListener("popupshowing", onPopupShow, false);

The border turns red with and without e10s enabled and both gContextMenu and gContextMenuContentData are always available when tested with FF Dev Edition (I didn’t test with any other versions/builds).

1 Like

Hmm, I am surprised that that works as well.

I had been thinking of the contentAreaContextMenu as being part of the browser chrome, so a part of the same process as the main add-on code. The menu needs to read in some data from its target when it is created so that it can be populated, so it made sense to me that it would have access to information about what was clicked on – the link URL if a link was clicked on or the image name if an image was clicked on, etc. However, I didn’t expect that with e10s enabled that the menu would have access to the page content document and be able to modify it. Going that far I would have thought would require a content script. It would be nice to hear from someone with more knowledge of the implementation of e10s. Like you, my knowledge is based on reading links like the one you provided and testing things out in Nightly with e10s enabled to see what works.

If you wanted to use https://developer.mozilla.org/en-US/Add-ons/SDK/High-Level_APIs/context-menu that you mentioned in your first post, it is possible. The only think is you have to work out how to load the sdk code. Something like this gets things started:

var { Loader } = Components.utils.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
var loader = Loader.Loader({
  modules: {
    "toolkit/loader": Loader
  },
  paths: {
    "devtools": "resource:///modules/devtools/",
    "sdk/": "resource://gre/modules/commonjs/sdk/",
    "": "resource://gre/modules/commonjs/"
  },
  rootURI: '',
  metadata: {
    'permissions': {
      'private-browsing': true
    }
  },
  resolve: function(id, base) {
    if (id == "chrome" || id.startsWith("@"))
      return id;
    return Loader.resolve(id, base);
  }
});

var module = Loader.Module("main", "");
var jetpack = Loader.Require(loader, module);

Then you can do something like this:

var cm = jetpack("sdk/context-menu");

And continue with the examples as given.

3 Likes

Impressive! Somehow I never thought of trying to load/re-use add-on SDK. It’s very nice to know that it can be done using add-on sdk but I wonder if there’s a smaller hammer that will work for this task: something that can provide similar functionality, work without add-on SDK and be e10s friendly.

Ultimatelly, it doesn’t even matter if the issue is with context menus or something else - what I’d like to know is, generally speaking, how is an add-on supposed to handle this type of scenarios where things are split between chrome and frame scripts (considering that use of sendSyncMessage doesn’t seem to be encouraged either).

After looking though the addon sdk context menu code, I came up with the following “hack” that relies on adding some data to gContextMenuContentData.addonInfo (again, I’d very much like to hear from Mozilla if/what is the proper way of doing this…).

chrome process (background.js) code:

function onPopupShow(ev) {
    let xulDocument = ev.currentTarget.ownerDocument;
    let gcm = xulDocument.defaultView.gContextMenu;
    let gcmcd = xulDocument.defaultView.gContextMenuContentData;
    if (gcmcd.addonInfo && gcmcd.addonInfo.myAddonData) {
        let payload = gcmcd.addonInfo.myAddonData;
        Services.console.logStringMessage("setting context options to "+payload.showOpt1+","+payload.showOpt2+","+payload.showOpt3);
    }
    let document = gcm.target.ownerDocument; //html document - can check content inside it
    document.body.style.border = "10px solid red";
}

var ctxMenu = wnd.document.getElementById("contentAreaContextMenu");
ctxMenu.addEventListener("popupshowing", onPopupShow, false);

Frame script code below:

function CMObserver(){
    this.observe = function(subject,topic,data){
        Services.console.logStringMessage("[!X!:content-contextmenu] observer " + Date.now());
        let opts = {};
        opts.showOpt1 = content.document.forms.length > 0;
        opts.showOpt2 = content.document.querySelectorAll("input[type=text]").length > 0;
        opts.showOpt3 = content.document.querySelectorAll("input[type=password]").length > 0;
        subject.wrappedJSObject.addonInfo.myAddonData = opts;
    };
this.register = function(topic){Services.obs.addObserver(this,topic,false);};
this.unregister = function(topic){Services.obs.removeObserver(this,topic);};
this.shutdown=function(){};
}

let cmobs = new CMObserver();
cmobs.register("content-contextmenu");

So, the steps are:

  1. chrome creates the menu items using document.createElement(“menuitem”) and then listens for “popupshowing”
  2. framescript registers an observer for “content-contextmenu”
  3. when context menu is about to be displayed, FF first calls the observer from the frame script and, after the observer is done, FF will trigger the event in chrome. This gets the frame script the chance to inspect the current document and decide which context menus should be displayed.

However, the frame script is not able to directly access the menu items so some way to pass “state” between frame and chrome scripts is required. sendSyncMessage might work but I’m not sure how…instead, I noticed that when the observer is called inside the frame script, the subject contains an empty??? “addonInfo” object (same one used by context-menu inside the add-on sdk) to which additional data can be attached and which will be visible (it’s allowed to cross the process boundary) in the chrome script.

It works and doesn’t look much more hackish than what seems to be going inside add-on SDK but does’t feel “right” somehow…

Does anyone know if accessing addonInfo though subject.wrappedJSObject is safe? (subject.wrappedJSObject.addonInfo.myAddonData = opts;) Any other way of doing it better?

The communication between a frame script and a chrome script should happen strictly through message passing. Anything else will likely break either now or in the near future. If we’re talking about an SDK add-on, however, none of this should be necessary. The SDK already has a way to inject scripts and communicate with them, which ultimately uses message passing.

Hi jorgev,

Thank you for taking the time to reply.

I understand that Mozilla is trying to encourage the use of Add-on SDK and that the “old” bootstrapped model is more difficult to use; at the same time, your own docs make no guarantee that add-on SDK based extensions will work with e10s unless this or that…and due to your rather tight release schedule (e10s and the signing of addons) I don’t afford the luxury of migrating everything to add-on SDK anyway.

At this point, please assume that I have a decent understanding of message passing/manager which I already use for a lot of other stuff going on between chrome and frame scripts.

The context menu issue boils down to this: the context menu is about to show up and I have to decide what menu items to display based on the page content (which should only be accessible from framescript - not really true, if you check my posts above). The context menu items however can not be modified from framescript so it has to be done from chrome. Framescript needs to send a message to chrome script - I’m OK with that as long as someone is willing to explain to me how will that work: the way I see it, if I send an async message (frame>chrome), I have zero guarantees that the message will arrive before chrome script’s own “popupshowing” event is triggered. If I send a sync message…my understanding is that I should avoid sending sync messages to chrome (especially since this is a pretty time sensitive case and it might delay the display of the context menu, make FF seem sluggish and/or result in a rejection during add-on review).

Since there’s basically zero documentation on how context menus are supposed to work in e10s, I don’t know which process is handling the menu and what is, internally, the sequence of events. Is chrome process (who manages the context menu window and items) the one which “catches” the event first, then maybe dispatch it to the content/page browser in some other process (frame) or is the frame catching the event first and them dispatches it to the chrome process?

Based on the discussion found here https://bugzilla.mozilla.org/show_bug.cgi?id=1060138, the issue seems to be far from trivial and your own patch to make add-on sdk context menus work with e10s is quite large and complex. It basically looks like message manager alone is not the solution.

Frankly, I don’t really want to know what FF does internally - it’s already unbelievable/unacceptable how much time I already spent on this “non issue” - I just want to know how to handle this case properly without add-on sdk (ideally through a core module created/supported by you but I’ll settle for anything that works). Is that possible?

Are you suggesting that Mozilla has no/little interest in supporting the “old” bootstrapped extensions any longer at this point and the only way forward is add-on sdk?

Will Michael’s solution above work and be accepted during add-on review?

Michael’s solution is acceptable. I don’t know how it would look like for bootstrapped extensions. The SDK is better documented and the solutions using the SDK tend to be much easier, which is a big reason we recommend it. Bootstrapped extensions (and overlay extensions for that matter) are still supported, but the documentation for them is spotty at best.