diff --git a/packages/addon-kit/docs/context-menu.md b/packages/addon-kit/docs/context-menu.md index 0b8ccb000..a06456e34 100644 --- a/packages/addon-kit/docs/context-menu.md +++ b/packages/addon-kit/docs/context-menu.md @@ -437,8 +437,10 @@ A labeled menu item that can perform an action when clicked. @property {string} - An arbitrary value associated with the menu item during creation. Currently - this property is read-only. + An optional arbitrary value to associate with the item. It must be either a + string or an object that implements `toString()`. It will be passed to + click listeners. You can set this after creating the item to update its data + later. @@ -449,6 +451,13 @@ A labeled menu item that can perform an action when clicked. contained in the top-level context menu. + +@property {Menu} + The item's parent `Menu`, or `null` if the item is contained in the top-level + context menu. This property is read-only. To add the item to a new menu, + call that menu's `addItem()` method. + + @property {string,array} The content script or the array of content scripts associated with the menu @@ -465,8 +474,10 @@ A labeled menu item that can perform an action when clicked. @method - Permanently removes the item from the top-level context menu. If the item is - not contained in the top-level context menu, this method does nothing. + Permanently removes the item from its parent menu and frees its resources. + The item must not be used afterward. If you need to remove the item from its + parent menu but use it afterward, call `removeItem()` on the parent menu + instead. @@ -527,8 +538,10 @@ A labeled menu item that expands into a submenu. @property {array} - The menu items contained in the menu. Currently the items in the menu cannot - be changed by modifying this property. + An array containing the items in the menu. The array is read-only, meaning + that modifications to it will not affect the menu. However, setting this + property to a new array will replace all the items currently in the menu with + the items in the new array. @@ -539,6 +552,13 @@ A labeled menu item that expands into a submenu. the top-level context menu. + +@property {Menu} + The menu's parent `Menu`, or `null` if the menu is contained in the top-level + context menu. This property is read-only. To add the menu to a new menu, + call that menu's `addItem()` method. + + @property {string,array} The content script or the array of content scripts associated with the menu @@ -553,10 +573,29 @@ A labeled menu item that expands into a submenu. top-level context menu. + +@method + Appends a menu item to the end of the menu. If the item is already contained + in another menu or in the top-level context menu, it's automatically removed + first. +@param item {Item,Menu,Separator} + The `Item`, `Menu`, or `Separator` to add to the menu. + + + +@method + Removes the given menu item from the menu. If the menu does not contain the + item, this method does nothing. +@param item {Item,Menu,Separator} + The menu item to remove from the menu. + + @method - Permanently removes the menu from the top-level context menu. If the menu is - not contained in the top-level context menu, this method does nothing. + Permanently removes the menu from its parent menu and frees its resources. + The menu must not be used afterward. If you need to remove the menu from its + parent menu but use it afterward, call `removeItem()` on the parent menu + instead. @@ -578,10 +617,26 @@ from the content script. The message can be any @class A menu separator. Separators can be contained only in `Menu`s, not in the top-level context menu. + @constructor Creates a menu separator. + + +@property {Menu} + The separator's parent `Menu`. This property is read-only. To add the + separator to a new menu, call that menu's `addItem()` method. + + + +@method + Permanently removes the separator from its parent menu and frees its + resources. The separator must not be used afterward. If you need to remove + the separator from its parent menu but use it afterward, call `removeItem()` + on the parent menu instead. + + diff --git a/packages/addon-kit/lib/context-menu.js b/packages/addon-kit/lib/context-menu.js index 70726f2cf..e41dd0b2e 100644 --- a/packages/addon-kit/lib/context-menu.js +++ b/packages/addon-kit/lib/context-menu.js @@ -57,6 +57,9 @@ const { MatchPattern } = require("match-pattern"); const { EventEmitterTrait: EventEmitter } = require("events"); const observerServ = require("observer-service"); const jpSelf = require("self"); +const winUtils = require("window-utils"); +const { Trait } = require("light-traits"); +const { Cortex } = require("cortex"); // All user items we add have this class name. const ITEM_CLASS = "jetpack-context-menu-item"; @@ -110,67 +113,298 @@ const NON_PAGE_CONTEXT_ELTS = [ Ci.nsIDOMHTMLTextAreaElement, ]; -// This object is used elsewhere in this file to access private properties of -// Item and Menu instances. -const PRIVATE_PROPS_KEY = { - valueOf: function valueOf() "private properties key" -}; +// This is used to access private properties of Item and Menu instances. +const PRIVATE_PROPS_KEY = Math.random().toString(); // Used as an internal ID for items and as part of a public ID for item DOM -// elements. +// elements. Careful: This number is not necessarily unique to any one instance +// of the module. For each module instance, when the first item is created this +// number will be 0, when the second is created it will be 1, and so on. let nextItemID = 0; -exports.Item = apiUtils.publicConstructor(Item); -exports.Menu = apiUtils.publicConstructor(Menu); -exports.Separator = apiUtils.publicConstructor(Separator); +exports.Item = Item; +exports.Menu = Menu; +exports.Separator = Separator; + + +// A word about traits and privates. `this` inside of traits methods is an +// object private to the implementation. It should never be publicly leaked. +// We use Cortex in the exported menu item constructors to create public +// reflections of the private objects that hide private properties -- those +// prefixed with an underscore. Public reflections are attached to the private +// objects via the `_public` property. +// +// All item objects passed into the implementation by the client will be public +// reflections, not private objects. Likewise, all item objects passed out of +// the implementation to the client must be public, not private. Mixing up +// public and private is bad and easy to do, so not only are private objects +// restricted to the implementation, but as much as possible we try to restrict +// them to the Item, Menu, and Separator traits and constructors. Everybody +// else in the implementation should expect to be passed public reflections, and +// they must specifically request private objects via privateItem(). + +// Item, Menu, and Separator are composed of this trait. +const ItemBaseTrait = Trait({ + + initBase: function IBT_initBase(opts, optRules, optsToNotSet) { + this._optRules = optRules; + for (let optName in optRules) + if (optsToNotSet.indexOf(optName) < 0) + this[optName] = opts[optName]; + optsToNotSet.forEach(function (opt) validateOpt(opts[opt], optRules[opt])); + this._isInited = true; + + this._id = nextItemID++; + this._parentMenu = null; + + // This makes the private properties accessible to anyone with access to + // PRIVATE_PROPS_KEY. Barring loader tricks, only this file has has access + // to it, so only this file has access to the private properties. + const self = this; + this.valueOf = function IBT_valueOf(key) { + return key === PRIVATE_PROPS_KEY ? self : self._public; + }; + }, + + destroy: function IBT_destroy() { + if (this._wasDestroyed) + return; + if (this.parentMenu) + this.parentMenu.removeItem(this._public); + else if (!(this instanceof Separator)) + browserManager.removeTopLevelItem(this._public); + browserManager.unregisterItem(this._public); + this._wasDestroyed = true; + }, + + get parentMenu() { + return this._parentMenu; + }, + + set parentMenu(val) { + throw new Error("The 'parentMenu' property is not intended to be set. " + + "Use menu.addItem(item) instead."); + }, + + set _isTopLevel(val) { + if (val) + this._workerReg = new WorkerRegistry(this._public); + else { + this._workerReg.destroy(); + delete this._workerReg; + } + }, + + get _topLevelItem() { + let topLevelItem = this._public; + let parentMenu = this.parentMenu; + while (parentMenu) { + topLevelItem = parentMenu; + parentMenu = parentMenu.parentMenu; + } + return topLevelItem; + } +}); + +// Item and Menu are composed of this trait. +const ActiveItemTrait = Trait.compose(ItemBaseTrait, EventEmitter, Trait({ + + initActiveItem: function AIT_initActiveItem(opts, optRules, optsToNotSet) { + this.initBase(opts, optRules, + optsToNotSet.concat(["onMessage", "context"])); + + if ("onMessage" in opts) + this.on("message", opts.onMessage); + + // When a URL context is removed (by calling context.remove(urlContext)), we + // may need to create workers for windows containing pages that the item now + // matches. Likewise, when a URL context is added, we need to destroy + // workers for windows containing pages that the item now does not match. + // + // collection doesn't provide a way to listen for removals. utils/registry + // does, but it doesn't allow its elements to be enumerated. So as a hack, + // use a collection for item.context and replace its add and remove methods. + collection.addCollectionProperty(this, "context"); + if (opts.context) + this.context.add(opts.context); + + const self = this; + + let add = this.context.add; + this.context.add = function itemContextAdd() { + let args = Array.slice(arguments); + add.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.destroyUnneededWorkers(); + }; + + let remove = this.context.remove; + this.context.remove = function itemContextRemove() { + let args = Array.slice(arguments); + remove.apply(self.context, args); + if (self._workerReg && args.some(function (a) a instanceof URLContext)) + self._workerReg.createNeededWorkers(); + }; + }, + + get label() { + return this._label; + }, + + set label(val) { + this._label = validateOpt(val, this._optRules.label); + if (this._isInited) + browserManager.setItemLabel(this, this._label); + return this._label; + }, + + get contentScript() { + return this._contentScript; + }, + + set contentScript(val) { + this._contentScript = validateOpt(val, this._optRules.contentScript); + return this._contentScript; + }, + + get contentScriptFile() { + return this._contentScriptFile; + }, + + set contentScriptFile(val) { + this._contentScriptFile = + validateOpt(val, this._optRules.contentScriptFile); + return this._contentScriptFile; + } +})); + +// Item is composed of this trait. +const ItemTrait = Trait.compose(ActiveItemTrait, Trait({ + + initItem: function IT_initItem(opts, optRules) { + this.initActiveItem(opts, optRules, []); + }, + + get data() { + return this._data; + }, + set data(val) { + this._data = validateOpt(val, this._optRules.data); + if (this._isInited) + browserManager.setItemData(this, this._data); + return this._data; + }, + + toString: function IT_toString() { + return '[object Item "' + this.label + '"]'; + } +})); +// The exported Item constructor. function Item(options) { - let rules = optionsRules(); - rules.data = { + let optRules = optionsRules(); + optRules.data = { map: function (v) v.toString(), is: ["string", "undefined"] }; - options = apiUtils.validateOptions(options, rules); - defineItemProps(this, options); + let item = ItemTrait.create(Item.prototype); + item.initItem(options, optRules); - // TODO: Add setter for this? - this.__defineGetter__("data", function () { - return "data" in options ? options.data : undefined; - }); - - this.toString = function Item_toString() { - return '[object Item "' + options.label + '"]'; - }; + item._public = Cortex(item); + browserManager.registerItem(item._public); + browserManager.addTopLevelItem(item._public); - browserManager.addItem(this); + return item._public; } +// Menu is composed of this trait. +const MenuTrait = Trait.compose(ActiveItemTrait, Trait({ + + initMenu: function MT_initMenu(opts, optRules, optsToNotSet) { + this._items = []; + this.initActiveItem(opts, optRules, optsToNotSet); + }, + + get items() { + return this._items; + }, + + set items(val) { + let newItems = validateOpt(val, this._optRules.items); + while (this._items.length) + this.removeItem(this._items[0]); + newItems.forEach(function (i) this.addItem(i), this); + return newItems; + }, + + addItem: function MT_addItem(item) { + // First, remove the item from its current parent. + if (item.parentMenu) + item.parentMenu.removeItem(item); + else if (!(item instanceof Separator)) + browserManager.removeTopLevelItem(item); + + // Now add the item to this menu. + this._items.push(item); + privateItem(item)._parentMenu = this._public; + browserManager.addItemToMenu(item, this._public); + }, + + removeItem: function MT_removeItem(item) { + let idx = this._items.indexOf(item); + if (idx < 0) + return; + this._items.splice(idx, 1); + privateItem(item)._parentMenu = null; + browserManager.removeItemFromMenu(item, this._public); + }, + + toString: function MT_toString() { + return '[object Menu "' + this.label + '"]'; + } +})); + +// The exported Menu constructor. function Menu(options) { - let rules = optionsRules(); - rules.items = { - is: ["array"] + let optRules = optionsRules(); + optRules.items = { + is: ["array"], + ok: function (v) { + return v.every(function (item) { + return (item instanceof Item) || + (item instanceof Menu) || + (item instanceof Separator); + }); + }, + msg: "items must be an array, and each element in the array must be an " + + "Item, Menu, or Separator." }; - options = apiUtils.validateOptions(options, rules); - defineItemProps(this, options); + let menu = MenuTrait.create(Menu.prototype); - // TODO: Add setter for this? - this.__defineGetter__("items", function () options.items.slice(0)); + // We can't rely on initBase to set the `items` property, because the menu + // needs to be registered with and added to the browserManager before any + // child items are added to it. + menu.initMenu(options, optRules, ["items"]); - this.toString = function Menu_toString() { - return '[object Menu "' + options.label + '"]'; - }; + menu._public = Cortex(menu); + browserManager.registerItem(menu._public); + browserManager.addTopLevelItem(menu._public); + menu.items = options.items; - options.items.forEach(function (i) browserManager.removeItem(i)); - browserManager.addItem(this); + return menu._public; } +// The exported Separator constructor. function Separator() { - this.toString = function Separator_toString() { - return "[object Separator]"; - }; + let sep = ItemBaseTrait.create(Separator.prototype); + sep.initBase({}, {}, []); + + sep._public = Cortex(sep); + browserManager.registerItem(sep._public); + return sep._public; } @@ -283,6 +517,12 @@ exports.SelectionContext = apiUtils.publicConstructor(SelectionContext); exports.URLContext = apiUtils.publicConstructor(URLContext); +// Returns a version of opt validated against the given rule. +function validateOpt(opt, rule) { + let { opt } = apiUtils.validateOptions({ opt: opt }, { opt: rule }); + return opt; +} + // Returns rules for apiUtils.validateOptions() common to Item and Menu. function optionsRules() { return { @@ -333,86 +573,6 @@ function optionsRules() { }; } -// Defines some getters and other properties that are common to Item and Menu. -// item is the Item or Menu object on which to define the properties, and -// options is a validated options object. -function defineItemProps(item, options) { - item.__defineGetter__("label", function () options.label); - item.__defineSetter__("label", function setItemLabel(val) { - let { label } = apiUtils.validateOptions({ label: val }, optionsRules()); - options.label = label; - browserManager.setItemLabel(item, label); - }); - - // Stupid ternaries to avoid Spidermonkey strict warnings. - item.__defineGetter__("contentScript", function () { - return "contentScript" in options ? options.contentScript : undefined; - }); - item.__defineGetter__("contentScriptFile", function () { - return "contentScriptFile" in options ? options.contentScriptFile : - undefined; - }); - - item.destroy = function Item_destroy() { - browserManager.removeItem(item); - }; - - // Create a private properties object for the item. - let privateProps = { - eventEmitter: EventEmitter.create(item), - workerReg: new WorkerRegistry(item), - id: nextItemID++ - }; - - // This makes the private properties accessible to anyone with access to the - // PRIVATE_PROPS_KEY object. Only this file has has access to it, so only - // this file has access to the private properties. - item.valueOf = function Item_valueOf(key) { - return key === PRIVATE_PROPS_KEY ? privateProps : item; - }; - - // Add all of privateProps.eventEmitter's own public methods to the item, - // binding them to eventEmitter. This will allow clients to treat the item as - // an event emitter. - Object.keys(privateProps.eventEmitter).forEach(function (key) { - if (key[0] !== "_") - item[key] = - privateProps.eventEmitter[key].bind(privateProps.eventEmitter); - }); - - // Register a message listener if one was passed to the constructor. - if ("onMessage" in options) - privateProps.eventEmitter.on("message", options.onMessage); - - // When a URL context is removed (by calling item.context.remove(urlContext)), - // we may need to create workers for windows containing pages that the item - // now matches. Likewise, when a URL context is added, we need to destroy - // workers for windows containing pages that the item now does not match. - // - // collection doesn't provide a way to listen for removals. utils/registry - // does, but it doesn't allow its elements to be enumerated. So as a hack, - // use a collection for item.context and replace its add and remove methods. - collection.addCollectionProperty(item, "context"); - if (options.context) - item.context.add(options.context); - - let add = item.context.add; - item.context.add = function itemContextAdd() { - let args = Array.slice(arguments); - add.apply(item.context, args); - if (args.some(function (a) a instanceof URLContext)) - privateProps.workerReg.destroyUnneededWorkers(); - }; - - let remove = item.context.remove; - item.context.remove = function itemContextRemove() { - let args = Array.slice(arguments); - remove.apply(item.context, args); - if (args.some(function (a) a instanceof URLContext)) - privateProps.workerReg.createNeededWorkers(); - }; -} - // Does a binary search on elts, a NodeList, and returns the DOM element // before which an item with targetLabel should be inserted. null is returned // if the new item should be inserted at the end. @@ -433,108 +593,26 @@ function insertionPoint(targetLabel, elts) { return elts[from] || null; } -// Builds an ID suitable for a DOM element from the given item ID. The optional -// suffix will be appended to the returned ID. -function domEltIDFromItemID(itemID, suffix) { - suffix = suffix || ""; - if (!/^[-a-z]*$/.test(suffix)) - throw new Error("Internal error: suffix must match the regexp [-a-z]*"); +// Builds an ID suitable for a DOM element from the given item ID. +// isInOverflowSubtree should be true if the returned element will be inserted +// into the DOM subtree rooted at the overflow menu. +function domEltIDFromItemID(itemID, isInOverflowSubtree) { + let suffix = isInOverflowSubtree ? "-overflow" : ""; return jpSelf.id + "-context-menu-item-" + itemID + suffix; } -// Parses the item ID out of the given DOM element ID and returns it. +// Parses the item ID out of the given DOM element ID and returns it. If the +// element's ID is malformed or it indicates that the element was not created by +// the instance of the module calling this function, returns -1. function itemIDFromDOMEltID(domEltID) { - return Number(/([0-9]+)[-a-z]*$/.exec(domEltID)[1]); + let match = /^(.+?)-context-menu-item-([0-9]+)[-a-z]*$/.exec(domEltID); + return !match || match[1] !== jpSelf.id ? -1 : match[2]; } - -// Keeps track of all browser windows. -let browserManager = { - items: [], - windows: [], - - // Registers an item with the manager. It's added to the context menus of - // all currently registered windows, and when new windows are registered it - // will be added to them, too. - addItem: function browserManager_addItem(item) { - this.items.push(item); - this.windows.forEach(function (w) w.addItems([item])); - }, - - // Sets the given item's label in all the browser windows. See - // ContextMenuPopup.setItemLabel. - setItemLabel: function browserManager_setItemLabel(item, label) { - this.windows.forEach(function (w) w.setItemLabel(item, label)); - }, - - // Registers the manager to listen for window openings and closings. Note - // that calling this method can cause onTrack to be called immediately if - // there are open windows. - init: function browserManager_init() { - require("unload").ensure(this); - let windowTracker = new (require("window-utils").WindowTracker)(this); - - // On inner-window-destroyed, remove the destroyed inner window's outer - // window from all items' worker registries. - observerServ.add("inner-window-destroyed", function observe(subj) { - let innerWinID = subj.QueryInterface(Ci.nsISupportsPRUint64).data; - this.items.forEach(function (item) { - let workerReg = item.valueOf(PRIVATE_PROPS_KEY).workerReg; - workerReg.unregisterContentWin(innerWinID); - }); - }, this); - }, - - // When the window tracker is unloaded, it'll call our onUntrack for every - // open browser window, so there's no need to do that here. The only other - // things to clean up are items and their worker registries. - unload: function browserManager_unload() { - this.items.forEach(function (item) { - item.valueOf(PRIVATE_PROPS_KEY).workerReg.destroy(); - }); - this.items.splice(0, this.items.length); - }, - - // Registers a window with the manager. This is a WindowTracker callback. - onTrack: function browserManager_onTrack(window) { - if (this._isBrowserWindow(window)) { - let win = new BrowserWindow(window); - this.windows.push(win); - win.addItems(this.items); - } - }, - - // Unregisters a window from the manager. It's told to undo all menu - // modifications. This is a WindowTracker callback. - onUntrack: function browserManager_onUntrack(window) { - if (this._isBrowserWindow(window)) { - for (let i = 0; i < this.windows.length; i++) { - if (this.windows[i].window == window) { - let win = this.windows.splice(i, 1)[0]; - win.destroy(); - return; - } - } - } - }, - - // Unregisters an item from the manager. It's removed from the context menus - // of all windows that are currently registered. If the item is not - // registered, this is a no-op. - removeItem: function browserManager_removeItem(item) { - let idx = this.items.indexOf(item); - if (idx >= 0) { - this.items.splice(idx, 1); - this.windows.forEach(function (w) w.removeItems([item])); - item.valueOf(PRIVATE_PROPS_KEY).workerReg.destroy(); - } - }, - - _isBrowserWindow: function browserManager__isBrowserWindow(win) { - let winType = win.document.documentElement.getAttribute("windowtype"); - return winType === "navigator:browser"; - } -}; +// Returns the private version of the given public reflection. +function privateItem(publicItem) { + return publicItem.valueOf(PRIVATE_PROPS_KEY); +} // A type of Worker tailored to our uses. @@ -592,7 +670,7 @@ WorkerRegistry.prototype = { // Registers a content window, creating a worker for it if it needs one. registerContentWin: function WR_registerContentWin(win) { - let innerWinID = this._innerWinID(win); + let innerWinID = winUtils.getInnerId(win); if (this._doesURLNeedWorker(win.document.URL)) this.winWorkers[innerWinID] = { win: win, worker: this._makeWorker(win) }; else @@ -637,13 +715,14 @@ WorkerRegistry.prototype = { // Returns the worker for the item-window pair or null if none exists. find: function WR_find(contentWin) { - let innerWinID = this._innerWinID(contentWin); + let innerWinID = winUtils.getInnerId(contentWin); return (innerWinID in this.winWorkers) ? this.winWorkers[innerWinID].worker : null; }, - // Unregisters all content windows in the registry, destroying all workers. + // Unregisters all content windows from the registry, which destroys all + // workers. destroy: function WR_destroy() { for (let innerWinID in this.winWorkers) this.unregisterContentWin(innerWinID); @@ -669,40 +748,169 @@ WorkerRegistry.prototype = { }); let (item = this.item) worker.on("message", function workerOnMessage(msg) { try { - let eventEmitter = item.valueOf(PRIVATE_PROPS_KEY).eventEmitter; - eventEmitter._emitOnObject(item, "message", msg); + privateItem(item)._emitOnObject(item, "message", msg); } catch (err) { console.exception(err); } }); return worker; + } +}; + + +// Mirrors state across all browser windows. +let browserManager = { + topLevelItems: [], + browserWins: [], + + // Call this when a new item is created, top-level or not. + registerItem: function BM_registerItem(item) { + this.browserWins.forEach(function (w) w.registerItem(item)); + }, + + // Call this when an item is destroyed and won't be used again, top-level or + // not. + unregisterItem: function BM_unregisterItem(item) { + this.browserWins.forEach(function (w) w.unregisterItem(item)); }, - _innerWinID: function WR__innerWinID(win) { - return win. - QueryInterface(Ci.nsIInterfaceRequestor). - getInterface(Ci.nsIDOMWindowUtils). - currentInnerWindowID; + addTopLevelItem: function BM_addTopLevelItem(item) { + this.topLevelItems.push(item); + privateItem(item)._isTopLevel = true; + this.browserWins.forEach(function (w) w.addTopLevelItem(item)); + }, + + removeTopLevelItem: function BM_removeTopLevelItem(item) { + let idx = this.topLevelItems.indexOf(item); + if (idx < 0) + throw new Error("Internal error: item not in top-level menu: " + item); + this.topLevelItems.splice(idx, 1); + this.browserWins.forEach(function (w) w.removeTopLevelItem(item)); + privateItem(item)._isTopLevel = false; + }, + + addItemToMenu: function BM_addItemToMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.addItemToMenu(item, parentMenu)); + }, + + removeItemFromMenu: function BM_removeItemFromMenu(item, parentMenu) { + this.browserWins.forEach(function (w) w.removeItemFromMenu(item, + parentMenu)); + }, + + setItemLabel: function BM_setItemLabel(item, label) { + this.browserWins.forEach(function (w) w.setItemLabel(item, label)); + }, + + setItemData: function BM_setItemData(item, data) { + this.browserWins.forEach(function (w) w.setItemData(item, data)); + }, + + // Note that calling this method will cause onTrack to be called immediately + // for each currently open browser window. + init: function BM_init() { + require("unload").ensure(this); + let windowTracker = new (require("window-utils").WindowTracker)(this); + + // On inner-window-destroyed, remove the destroyed inner window's outer + // window from all items' worker registries. + observerServ.add("inner-window-destroyed", function observe(subj) { + let innerWinID = subj.QueryInterface(Ci.nsISupportsPRUint64).data; + this.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.unregisterContentWin(innerWinID); + }); + }, this); + }, + + unload: function BM_unload() { + // The window tracker is unloaded at the same time this method is called, + // which causes onUntrack to be called for each open browser window, so + // there's no need to clean up browser windows here. + + while (this.topLevelItems.length) { + let item = this.topLevelItems[0]; + this.removeTopLevelItem(item); + this.unregisterItem(item); + } + }, + + // Registers a browser window with the manager. This is a WindowTracker + // callback. + onTrack: function BM_onTrack(window) { + if (!this._isBrowserWindow(window)) + return; + + let browserWin = new BrowserWindow(window); + this.browserWins.push(browserWin); + + // Add all top-level items and, recursively, their child items to the new + // browser window. + function addItemTree(item, parentMenu) { + browserWin.registerItem(item); + if (parentMenu) + browserWin.addItemToMenu(item, parentMenu); + else + browserWin.addTopLevelItem(item); + if (item instanceof Menu) + item.items.forEach(function (subitem) addItemTree(subitem, item)); + } + this.topLevelItems.forEach(function (item) addItemTree(item, null)); + }, + + // Unregisters a browser window from the manager. This is a WindowTracker + // callback. + onUntrack: function BM_onUntrack(window) { + if (!this._isBrowserWindow(window)) + return; + + // Remove the window from the window list. + let idx = 0; + for (; idx < this.browserWins.length; idx++) + if (this.browserWins[idx].window == window) + break; + if (idx == this.browserWins.length) + throw new Error("Internal error: browser window not found"); + let browserWin = this.browserWins.splice(idx, 1)[0]; + + // Remove all top-level items from the window. + this.topLevelItems.forEach(function (i) browserWin.removeTopLevelItem(i)); + browserWin.destroy(); + }, + + _isBrowserWindow: function BM__isBrowserWindow(win) { + let winType = win.document.documentElement.getAttribute("windowtype"); + return winType === "navigator:browser"; } }; -// Keeps track of a single browser window. Responsible for providing a -// description of the window's current context and determining whether an item -// matches the current context. +// Responsible for creating and managing context menu item DOM elements for a +// browser window. Also responsible for providing a description of the window's +// current context and determining whether an item matches the current context. // // TODO: If other apps besides Firefox want to support the context menu in -// whatever way is appropriate for them, plugging in a substitute for this class -// should be the way to do it. Make it easy for them. See bug 560716. +// whatever way is appropriate for them, plugging in a substitute for or an +// adapter to this class should be the way to do it. Make it easy for them. +// See bug 560716. function BrowserWindow(window) { this.window = window; this.doc = window.document; - let popup = this.doc.getElementById("contentAreaContextMenu"); - if (!popup) + let popupDOMElt = this.doc.getElementById("contentAreaContextMenu"); + if (!popupDOMElt) throw new Error("Internal error: Context menu popup not found."); - this.contextMenuPopup = new ContextMenuPopup(popup, this); + this.contextMenuPopup = new ContextMenuPopup(popupDOMElt, this); + + // item ID => { item, domElt, overflowDOMElt, popup, overflowPopup } + // item may or may not be top-level. domElt is the item's DOM element + // contained in the subtree rooted in the top-level context menu. + // overflowDOMElt is the item's DOM element contained in the subtree rooted in + // the overflow submenu. popup and overflowPopup are only defined if the item + // is a Menu; they're the Popup instances containing the Menu's child items, + // with the aforementioned distinction between top-level and overflow + // subtrees. + this.items = {}; // Listen for page loads on the tabbrowser so we can create workers. window.gBrowser.addEventListener("DOMContentLoaded", this, false); @@ -710,26 +918,67 @@ function BrowserWindow(window) { BrowserWindow.prototype = { - // Adds an array of items to the window's context menu. - addItems: function BW_addItems(items) { - this.contextMenuPopup.addItems(items); - - // Register all open and loaded content windows in this browser window with - // each item's worker registry. - items.forEach(function (item) { - this.window.gBrowser.browsers.forEach(function (browser) { - if (browser.contentDocument.readyState === "complete") { - item.valueOf(PRIVATE_PROPS_KEY).workerReg. - registerContentWin(browser.contentWindow); - } - }, this); + // Creates and stores DOM elements for the given item, top-level or not. + registerItem: function BW_registerItem(item) { + // this.items[id] is referenced by _makeMenu, so it needs to be defined + // before _makeDOMElt is called. + let props = { item: item }; + this.items[privateItem(item)._id] = props; + props.domElt = this._makeDOMElt(item, false); + props.overflowDOMElt = this._makeDOMElt(item, true); + }, + + // Removes the given item's DOM elements from the store. + unregisterItem: function BW_unregisterItem(item) { + delete this.items[privateItem(item)._id]; + }, + + addTopLevelItem: function BW_addTopLevelItem(item) { + // Register all open and loaded content windows in this browser window + // with the item's worker registry. + let workerReg = privateItem(item)._workerReg; + this.window.gBrowser.browsers.forEach(function (browser) { + if (browser.contentDocument.readyState === "complete") + workerReg.registerContentWin(browser.contentWindow); }, this); + this.contextMenuPopup.addItem(item); + }, + + removeTopLevelItem: function BW_removeTopLevelItem(item) { + // Although addTopLevelItem registers this browser's content windows with + // the item's worker registry, there's no need to unregister content windows + // here: When a top-level item is removed, browserManager destroys its + // worker registry, which unregisters all registered content windows. + + this.contextMenuPopup.removeItem(item); + }, + + addItemToMenu: function BW_addItemToMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.addItem(item); + overflowPopup.addItem(item); + }, + + removeItemFromMenu: function BW_removeItemFromMenu(item, parentMenu) { + let { popup, overflowPopup } = this.items[privateItem(parentMenu)._id]; + popup.removeItem(item); + overflowPopup.removeItem(item); }, - // Sets the given item's label in the browser window's context menu. See - // ContextMenuPopup.setItemLabel. setItemLabel: function BW_setItemLabel(item, label) { - this.contextMenuPopup.setItemLabel(item, label); + let itemID = privateItem(item)._id; + let { domElt, overflowDOMElt } = this.items[itemID]; + domElt.setAttribute("label", label); + overflowDOMElt.setAttribute("label", label); + if (!item.parentMenu) + this.contextMenuPopup.itemLabelDidChange(item); + }, + + setItemData: function BW_setItemData(item, data) { + let itemID = privateItem(item)._id; + let { domElt, overflowDOMElt } = this.items[itemID]; + domElt.setAttribute("value", data); + overflowDOMElt.setAttribute("value", data); }, // The context specified for a top-level item may not match exactly the real @@ -755,7 +1004,7 @@ BrowserWindow.prototype = { // Returns true if all of item's contexts are current in the window. areAllContextsCurrent: function BW_areAllContextsCurrent(item, popupNode) { let win = popupNode.ownerDocument.defaultView; - let worker = item.valueOf(PRIVATE_PROPS_KEY).workerReg.find(win); + let worker = privateItem(item)._workerReg.find(win); // If the worker for the item-window pair doesn't exist (e.g., because the // page hasn't loaded yet), we can't really make a good decision since the @@ -791,28 +1040,24 @@ BrowserWindow.prototype = { return this.popupNode; }, - // Undoes all modifications to the window's context menu. The BrowserWindow - // should not be used afterward. destroy: function BW_destroy() { this.contextMenuPopup.destroy(); this.window.gBrowser.removeEventListener("DOMContentLoaded", this, false); + delete this.window; + delete this.doc; + delete this.items; }, - // Emits a click event in the port of the content worker related to item and - // popupNode's content window. Listeners will be passed popupNode and - // clickedItemData. - fireClick: function BW_fireClick(item, popupNode, clickedItemData) { + // Emits a click event in the port of the content worker related to given top- + // level item and popupNode's content window. Listeners will be passed + // popupNode and clickedItemData. + fireClick: function BW_fireClick(topLevelItem, popupNode, clickedItemData) { let win = popupNode.ownerDocument.defaultView; - let worker = item.valueOf(PRIVATE_PROPS_KEY).workerReg.find(win); + let worker = privateItem(topLevelItem)._workerReg.find(win); if (worker) worker.fireClick(popupNode, clickedItemData); }, - // Removes an array of items from the window's context menu. - removeItems: function BW_removeItems(items) { - this.contextMenuPopup.removeItems(items); - }, - // Handles content window loads. handleEvent: function BW_handleEvent(event) { try { @@ -828,121 +1073,39 @@ BrowserWindow.prototype = { } }, - _registerContentWin: function BW__registerContentWin(win) { - browserManager.items.forEach(function (item) { - item.valueOf(PRIVATE_PROPS_KEY).workerReg.registerContentWin(win); - }); - } -}; - - -// Represents a container of items that's the child of the given Menu and Popup. -// popupElt is a xul:menupopup that represents the popup in the DOM, and window -// is the BrowserWindow containing the popup. The popup is responsible for -// creating and adding items to poupElt and handling command events. -function Popup(parentMenu, parentPopup, popupElt, window) { - this.parentMenu = parentMenu; - this.parentPopup = parentPopup; - this.popupElt = popupElt; - this.window = window; - this.doc = popupElt.ownerDocument; - - // item ID => { item, domElt } - this.items = {}; - - popupElt.addEventListener("command", this, false); -} - -Popup.prototype = { - - // Adds an array of items to the popup. - addItems: function Popup_addItems(items) { - items.forEach(function (item) { - let domElt = this._makeItemElt(item); - this.items[item.valueOf(PRIVATE_PROPS_KEY).id] = { - item: item, - domElt: domElt - }; - this.popupElt.appendChild(domElt); - }, this); - }, - - // Undoes all modifications to the popup. The popup should not be used - // afterward. - destroy: function Popup_destroy() { - this.popupElt.removeEventListener("command", this, false); - }, - - // The popup is responsible for two command events: those originating at items - // in the popup and those bubbling to the popup's parent menu. In the first - // case the popup dispatches a click to the item, and in the second the popup - // dispatches a click to its parent menu -- in that order. - handleEvent: function Popup_handleEvent(event) { - try { - let elt = event.target; - if (elt.className.split(/\s+/).indexOf(ITEM_CLASS) >= 0) { - // If the event originated at an item in the popup, dispatch a click. - // Also set Popup.clickedItem and popupNode so ancestor popups know - // which item was clicked and under what context. - let itemID = itemIDFromDOMEltID(elt.id); - if (itemID in this.items) { - let clickedItem = this.items[itemID].item; - let topLevelItem = this._topLevelItem(clickedItem); - let popupNode = this.window.adjustPopupNode(this.window.popupNode, - topLevelItem); - Popup.clickedItem = clickedItem; - Popup.popupNode = popupNode; - this.window.fireClick(clickedItem, popupNode, clickedItem.data); - } - - // Dispatch a click to this popup's parent menu. - if (this.parentMenu) { - this.window.fireClick(this.parentMenu, Popup.popupNode, - Popup.clickedItem.data); - } - } - } - catch (err) { - console.exception(err); - } - }, - - // Returns a DOM element representing the item. All elements will have the - // ITEM_CLASS class, and className can optionally be used to add another. The - // element will have a unique ID. idSuffix, if given, will be appended to the - // ID. - _makeItemElt: function Popup__makeItemElt(item, className, idSuffix) { - let elt = item instanceof Item ? this._makeMenuitem(item, className) : - item instanceof Menu ? this._makeMenu(item, className) : - item instanceof Separator ? this._makeSeparator(className) : + _makeDOMElt: function BW__makeDOMElt(item, isInOverflowSubtree) { + let elt = item instanceof Item ? this._makeMenuitem(item) : + item instanceof Menu ? this._makeMenu(item, isInOverflowSubtree) : + item instanceof Separator ? this._makeSeparator() : null; if (!elt) throw new Error("Internal error: can't make element, unknown item type"); - elt.id = domEltIDFromItemID(item.valueOf(PRIVATE_PROPS_KEY).id, idSuffix); + elt.id = domEltIDFromItemID(privateItem(item)._id, isInOverflowSubtree); + elt.className = ITEM_CLASS; return elt; }, // Returns a new xul:menu representing the menu. - _makeMenu: function Popup__makeMenu(menu, className) { + _makeMenu: function BW__makeMenu(menu, isInOverflowSubtree) { let menuElt = this.doc.createElement("menu"); - menuElt.className = ITEM_CLASS + (className ? " " + className : ""); menuElt.setAttribute("label", menu.label); - let popupElt = this.doc.createElement("menupopup"); - menuElt.appendChild(popupElt); + let popupDOMElt = this.doc.createElement("menupopup"); + menuElt.appendChild(popupDOMElt); - // Once items are added, this value can be thrown away. The popup handles - // popupshowing on its own. - let popup = new Popup(menu, this, popupElt, this.window); - popup.addItems(menu.items); + let popup = new Popup(popupDOMElt, this, isInOverflowSubtree); + let props = this.items[privateItem(menu)._id]; + if (isInOverflowSubtree) + props.overflowPopup = popup; + else + props.popup = popup; return menuElt; }, // Returns a new xul:menuitem representing the item. - _makeMenuitem: function Popup__makeMenuitem(item, className) { + _makeMenuitem: function BW__makeMenuitem(item) { let elt = this.doc.createElement("menuitem"); - elt.className = ITEM_CLASS + (className ? " " + className : ""); elt.setAttribute("label", item.label); if (item.data) elt.setAttribute("value", item.data); @@ -950,259 +1113,277 @@ Popup.prototype = { }, // Returns a new xul:menuseparator. - _makeSeparator: function Popup__makeSeparator(className) { - let elt = this.doc.createElement("menuseparator"); - elt.className = ITEM_CLASS + (className ? " " + className : ""); - return elt; + _makeSeparator: function BW__makeSeparator() { + return this.doc.createElement("menuseparator"); }, - // Returns the top-level menu that contains item or item if it is top-level. - _topLevelItem: function Popup__topLevelItem(item) { - let popup = this; - let topLevelItem = item; - while (popup.parentPopup) { - topLevelItem = popup.parentMenu; - popup = popup.parentPopup; - } - return topLevelItem; + _registerContentWin: function BW__registerContentWin(win) { + browserManager.topLevelItems.forEach(function (item) { + privateItem(item)._workerReg.registerContentWin(win); + }); } }; -// A subclass of Popup, this represents a window's context menu popup. It's -// responsible for hiding and showing items according to the window's current -// context. -function ContextMenuPopup(popupElt, window) { - const self = this; - Popup.call(this, null, null, popupElt, window); +// Responsible for adding DOM elements to and removing them from poupDOMElt. +function Popup(popupDOMElt, browserWin, isInOverflowSubtree) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.isInOverflowSubtree = isInOverflowSubtree; +} - // Adds an array of items to the popup. - this.addItems = function CMP_addItems(items) { - if (!items.length) - return; +Popup.prototype = { - ensureStaticEltsExist(); - ensureListeningForPopups(); - - // Add each item to the top-level menu and the overflow submenu. - items.forEach(function (item) { - let itemID = item.valueOf(PRIVATE_PROPS_KEY).id; - let domElt = self._makeItemElt(item, TOPLEVEL_ITEM_CLASS); - let overflowDOMElt = self._makeItemElt(item, OVERFLOW_ITEM_CLASS, - "-overflow"); - self.items[itemID] = { - item: item, - domElt: domElt, - overflowDOMElt: overflowDOMElt - }; - insertItemInSortedOrder(item); - }, self); - }; + addItem: function Popup_addItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.appendChild(elt); + }, - // Sets the given item's label if the item has a DOM element. The item is - // re-inserted into the popup so that it remains in sorted order. If the item - // has no DOM element yet, does nothing. - this.setItemLabel = function CMP_setItemLabel(item, label) { - let itemID = item.valueOf(PRIVATE_PROPS_KEY).id; - if (!(itemID in self.items)) - return; + removeItem: function Popup_removeItem(item) { + let props = this.browserWin.items[privateItem(item)._id]; + let elt = this.isInOverflowSubtree ? props.overflowDOMElt : props.domElt; + this.popupDOMElt.removeChild(elt); + } +}; - let { domElt, overflowDOMElt } = self.items[itemID]; - domElt.parentNode.removeChild(domElt); - overflowDOMElt.parentNode.removeChild(overflowDOMElt); - domElt.setAttribute("label", label); - overflowDOMElt.setAttribute("label", label); - insertItemInSortedOrder(item); - }; - // Undoes all modifications to the popup. The popup should not be used - // afterward. - this.destroy = function CMP_destroy() { - for each (let { item } in self.items) - self.removeItems([item]); +// Represents a browser window's context menu popup. Responsible for hiding and +// showing items according to the browser window's current context and for +// handling item clicks. +function ContextMenuPopup(popupDOMElt, browserWin) { + this.popupDOMElt = popupDOMElt; + this.browserWin = browserWin; + this.doc = popupDOMElt.ownerDocument; + + // item ID => item + // Calling this variable "topLevelItems" is redundant, since Popup and + // ContextMenuPopup are only responsible for their child items, not all their + // descendant items. But calling it "items" might encourage one to believe + // otherwise, so topLevelItems it is. + this.topLevelItems = {}; + + popupDOMElt.addEventListener("popupshowing", this, false); + popupDOMElt.addEventListener("command", this, false); +} + +ContextMenuPopup.prototype = { + addItem: function CMP_addItem(item) { + this._ensureStaticEltsExist(); + let itemID = privateItem(item)._id; + this.topLevelItems[itemID] = item; + let props = this.browserWin.items[itemID]; + props.domElt.classList.add(TOPLEVEL_ITEM_CLASS); + props.overflowDOMElt.classList.add(OVERFLOW_ITEM_CLASS); + this._insertItemInSortedOrder(item); + }, + + removeItem: function CMP_removeItem(item) { + let itemID = privateItem(item)._id; + delete this.topLevelItems[itemID]; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.classList.remove(TOPLEVEL_ITEM_CLASS); + overflowDOMElt.classList.remove(OVERFLOW_ITEM_CLASS); + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + }, + + // Call this after the item's label changes. This re-inserts the item into + // the popup so that it remains in sorted order. + itemLabelDidChange: function CMP_itemLabelDidChange(item) { + let itemID = privateItem(item)._id; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + this.popupDOMElt.removeChild(domElt); + this._overflowPopup.removeChild(overflowDOMElt); + this._insertItemInSortedOrder(item); + }, + + destroy: function CMP_destroy() { // If there are no more items from any instance of the module, remove the // separator and overflow submenu, if they exist. - let elts = topLevelElts(); + let elts = this._topLevelElts; if (!elts.length) { - let submenu = overflowMenu(); + let submenu = this._overflowMenu; if (submenu) - self.popupElt.removeChild(submenu); + this.popupDOMElt.removeChild(submenu); - let sep = separator(); + let sep = this._separator; if (sep) - self.popupElt.removeChild(sep); + this.popupDOMElt.removeChild(sep); } - // Remove event listeners. - if (self._listeningForPopups) { - self.popupElt.removeEventListener("popupshowing", self, false); - delete self._listeningForPopups; + this.popupDOMElt.removeEventListener("popupshowing", this, false); + this.popupDOMElt.removeEventListener("command", this, false); + }, + + handleEvent: function CMP_handleEvent(event) { + try { + if (event.type === "command") + this._handleClick(event.target); + else if (event.type === "popupshowing" && + event.target === this.popupDOMElt) + this._handlePopupShowing(); } - self.__proto__.destroy.call(self); - }; + catch (err) { + console.exception(err); + } + }, - // The context menu popup needs to handle popupshowing in addition to command - // events. popupshowing is used to show top-level items that match the + // command events bubble to the context menu's top-level xul:menupopup and are + // caught here. + _handleClick: function CMP__handleClick(clickedDOMElt) { + if (!clickedDOMElt.classList.contains(ITEM_CLASS)) + return; + let itemID = itemIDFromDOMEltID(clickedDOMElt.id); + if (itemID < 0) + return; + let { item, domElt, overflowDOMElt } = this.browserWin.items[itemID]; + + // Bail if the DOM element was not created by this module instance. In + // real-world add-ons, the itemID < 0 check above is sufficient, but for the + // unit test the JID never changes, making this necessary. + if (clickedDOMElt != domElt && clickedDOMElt != overflowDOMElt) + return; + + let topLevelItem = privateItem(item)._topLevelItem; + let popupNode = this.browserWin.adjustPopupNode(this.browserWin.popupNode, + topLevelItem); + this.browserWin.fireClick(topLevelItem, popupNode, item.data); + }, + + // popupshowing is used to show top-level items that match the browser // window's current context and hide items that don't. Each module instance // is responsible for showing and hiding the items it owns. - this.handleEvent = function CMP_handleEvent(event) { - if (event.type === "command") - self.__proto__.handleEvent.call(self, event); - else if (event.type === "popupshowing" && event.target === popupElt) { - try { - // popupElt.triggerNode was added in Gecko 2.0 by bug 383930. The || is - // to avoid a Spidermonkey strict warning on earlier versions. - let triggerNode = popupElt.triggerNode || undefined; - let popupNode = window.capturePopupNode(triggerNode); - - // Show and hide items. Set a "jetpackContextCurrent" property on the - // DOM elements to signal which of our items match the current context. - for each (let { item, domElt, overflowDOMElt } in self.items) { - let areContextsCurr = window.areAllContextsCurrent(item, popupNode); - - // Change the item's label if the return value was a string. - if (typeof(areContextsCurr) === "string") { - item.label = areContextsCurr; - areContextsCurr = true; - } - - domElt.jetpackContextCurrent = areContextsCurr; - domElt.hidden = !areContextsCurr; - overflowDOMElt.jetpackContextCurrent = areContextsCurr; - overflowDOMElt.hidden = !areContextsCurr; - } - - // Get the total number of items that match the current context. It's a - // little tricky: There may be other instances of this module loaded, - // each hiding and showing their items. So we can't base this number on - // only our items, or on the hidden state of items. That's why we set - // the jetpackContextCurrent property above. The last instance to run - // will leave the menupopup in the correct state. - let elts = topLevelElts(); - let numShown = Array.reduce(elts, function (total, elt) { - return total + (elt.jetpackContextCurrent ? 1 : 0); - }, 0); - - // If too many items are shown, show the submenu and hide the top-level - // items. Otherwise, hide the submenu and show the top-level items. - let overflow = numShown > overflowThreshold(); - if (overflow) - Array.forEach(elts, function (e) e.hidden = true); - - let submenu = overflowMenu(); - if (submenu) - submenu.hidden = !overflow; - - // If no items are shown, hide the menu separator. - let sep = separator(); - if (sep) - sep.hidden = numShown === 0; - } - catch (err) { - console.exception(err); + _handlePopupShowing: function CMP__handlePopupShowing() { + // popupDOMElt.triggerNode was added in Gecko 2.0 by bug 383930. The || is + // to avoid a Spidermonkey strict warning on earlier versions. + let triggerNode = this.popupDOMElt.triggerNode || undefined; + let popupNode = this.browserWin.capturePopupNode(triggerNode); + + // Show and hide items. Set a "jetpackContextCurrent" property on the + // DOM elements to signal which of our items match the current context. + for (let [itemID, item] in Iterator(this.topLevelItems)) { + let areContextsCurr = + this.browserWin.areAllContextsCurrent(item, popupNode); + + // Change the item's label if the return value was a string. + if (typeof(areContextsCurr) === "string") { + item.label = areContextsCurr; + areContextsCurr = true; } - } - }; - // Removes an array of items from the popup. - this.removeItems = function CMP_removeItems(items) { - items.forEach(function (item) { - let itemID = item.valueOf(PRIVATE_PROPS_KEY).id; - let { domElt, overflowDOMElt } = self.items[itemID]; - domElt.parentNode.removeChild(domElt); - overflowDOMElt.parentNode.removeChild(overflowDOMElt); - delete self.items[itemID]; - }, self); - }; - - // Adds the popupshowing listener if it hasn't been added already. - function ensureListeningForPopups() { - if (!self._listeningForPopups) { - self.popupElt.addEventListener("popupshowing", self, false); - self._listeningForPopups = true; + let { domElt, overflowDOMElt } = this.browserWin.items[itemID]; + domElt.jetpackContextCurrent = areContextsCurr; + domElt.hidden = !areContextsCurr; + overflowDOMElt.jetpackContextCurrent = areContextsCurr; + overflowDOMElt.hidden = !areContextsCurr; } - } + + // Get the total number of items that match the current context. It's a + // little tricky: There may be other instances of this module loaded, + // each hiding and showing their items. So we can't base this number on + // only our items, or on the hidden state of items. That's why we set + // the jetpackContextCurrent property above. The last instance to run + // will leave the menupopup in the correct state. + let elts = this._topLevelElts; + let numShown = Array.reduce(elts, function (total, elt) { + return total + (elt.jetpackContextCurrent ? 1 : 0); + }, 0); + + // If too many items are shown, show the submenu and hide the top-level + // items. Otherwise, hide the submenu and show the top-level items. + let overflow = numShown > this._overflowThreshold; + if (overflow) + Array.forEach(elts, function (e) e.hidden = true); + + let submenu = this._overflowMenu; + if (submenu) + submenu.hidden = !overflow; + + // If no items are shown, hide the menu separator. + let sep = this._separator; + if (sep) + sep.hidden = numShown === 0; + }, // Adds the menu separator and overflow submenu if they don't exist. - function ensureStaticEltsExist() { - let sep = separator(); + _ensureStaticEltsExist: function CMP__ensureStaticEltsExist() { + let sep = this._separator; if (!sep) { - sep = makeSeparator(); - self.popupElt.appendChild(sep); + sep = this._makeSeparator(); + this.popupDOMElt.appendChild(sep); } - let submenu = overflowMenu(); + let submenu = this._overflowMenu; if (!submenu) { - submenu = makeOverflowMenu(); - self.popupElt.insertBefore(submenu, sep.nextSibling); + submenu = this._makeOverflowMenu(); + this.popupDOMElt.insertBefore(submenu, sep.nextSibling); } - } + }, // Inserts the given item's DOM element into the popup in sorted order. - function insertItemInSortedOrder(item) { - let itemID = item.valueOf(PRIVATE_PROPS_KEY).id; - self.popupElt.insertBefore(self.items[itemID].domElt, - insertionPoint(item.label, topLevelElts())); - overflowPopup().insertBefore(self.items[itemID].overflowDOMElt, - insertionPoint(item.label, overflowElts())); - } + _insertItemInSortedOrder: function CMP__insertItemInSortedOrder(item) { + let props = this.browserWin.items[privateItem(item)._id]; + this.popupDOMElt.insertBefore( + props.domElt, insertionPoint(item.label, this._topLevelElts)); + this._overflowPopup.insertBefore( + props.overflowDOMElt, insertionPoint(item.label, this._overflowElts)); + }, // Creates and returns the xul:menu that's shown when too many items are added // to the popup. - function makeOverflowMenu() { - let submenu = self.doc.createElement("menu"); + _makeOverflowMenu: function CMP__makeOverflowMenu() { + let submenu = this.doc.createElement("menu"); submenu.id = OVERFLOW_MENU_ID; submenu.setAttribute("label", OVERFLOW_MENU_LABEL); - let popup = self.doc.createElement("menupopup"); + let popup = this.doc.createElement("menupopup"); popup.id = OVERFLOW_POPUP_ID; submenu.appendChild(popup); return submenu; - } + }, // Creates and returns the xul:menuseparator that separates the standard // context menu items from our items. - function makeSeparator() { - let elt = self.doc.createElement("menuseparator"); + _makeSeparator: function CMP__makeSeparator() { + let elt = this.doc.createElement("menuseparator"); elt.id = SEPARATOR_ID; return elt; - } + }, // Returns the item elements contained in the overflow menu, a NodeList. - function overflowElts() { - return overflowPopup().getElementsByClassName(OVERFLOW_ITEM_CLASS); - } + get _overflowElts() { + return this._overflowPopup.getElementsByClassName(OVERFLOW_ITEM_CLASS); + }, // Returns the overflow xul:menu. - function overflowMenu() { - return self.doc.getElementById(OVERFLOW_MENU_ID); - } + get _overflowMenu() { + return this.doc.getElementById(OVERFLOW_MENU_ID); + }, // Returns the overflow xul:menupopup. - function overflowPopup() { - return self.doc.getElementById(OVERFLOW_POPUP_ID); - } + get _overflowPopup() { + return this.doc.getElementById(OVERFLOW_POPUP_ID); + }, // Returns the OVERFLOW_THRESH_PREF pref value if it exists or // OVERFLOW_THRESH_DEFAULT if it doesn't. - function overflowThreshold() { + get _overflowThreshold() { let prefs = require("preferences-service"); return prefs.get(OVERFLOW_THRESH_PREF, OVERFLOW_THRESH_DEFAULT); - } + }, // Returns the xul:menuseparator. - function separator() { - return self.doc.getElementById(SEPARATOR_ID); - } + get _separator() { + return this.doc.getElementById(SEPARATOR_ID); + }, // Returns the item elements contained in the top-level menu, a NodeList. - function topLevelElts() { - return self.popupElt.getElementsByClassName(TOPLEVEL_ITEM_CLASS); + get _topLevelElts() { + return this.popupDOMElt.getElementsByClassName(TOPLEVEL_ITEM_CLASS); } }; -ContextMenuPopup.prototype = Popup.prototype; - // Init the browserManager only after setting prototypes and such above, because // it will cause browserManager.onTrack to be called immediately if there are diff --git a/packages/addon-kit/tests/test-context-menu.js b/packages/addon-kit/tests/test-context-menu.js index 3e3ffab56..b554e4578 100644 --- a/packages/addon-kit/tests/test-context-menu.js +++ b/packages/addon-kit/tests/test-context-menu.js @@ -60,6 +60,7 @@ exports.testConstructDestroy = function (test) { // Create an item. let item = new loader.cm.Item({ label: "item" }); + test.assertEqual(item.parentMenu, null, "item's parent menu should be null"); test.showMenu(null, function (popup) { @@ -67,7 +68,8 @@ exports.testConstructDestroy = function (test) { test.checkMenu([item], [], []); popup.hidePopup(); - // Destroy the item. + // Destroy the item. Multiple destroys should be harmless. + item.destroy(); item.destroy(); test.showMenu(null, function (popup) { @@ -389,7 +391,7 @@ exports.testURLContextAdd = function (test) { test.withTestDoc(function (window, doc) { let privatePropsKey = loader.globalScope.PRIVATE_PROPS_KEY; - let workerReg = item.valueOf(privatePropsKey).workerReg; + let workerReg = item.valueOf(privatePropsKey)._workerReg; let found = false; for each (let winWorker in workerReg.winWorkers) { @@ -963,6 +965,37 @@ exports.testMenuClick = function (test) { }); }; + +// Click listeners should work when multiple modules are loaded. +exports.testItemClickMultipleModules = function (test) { + test = new TestHelper(test); + let loader0 = test.newLoader(); + let loader1 = test.newLoader(); + + let item0 = loader0.cm.Item({ + label: "loader 0 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.fail("loader 0 item should not emit click event"); + } + }); + let item1 = loader1.cm.Item({ + label: "loader 1 item", + contentScript: 'self.on("click", self.postMessage);', + onMessage: function () { + test.pass("loader 1 item clicked as expected"); + test.done(); + } + }); + + test.showMenu(null, function (popup) { + test.checkMenu([item0, item1], [], []); + let item1Elt = test.getItemElt(popup, item1); + item1Elt.click(); + }); +}; + + // Adding a separator to a submenu should work OK. exports.testSeparator = function (test) { test = new TestHelper(test); @@ -975,18 +1008,6 @@ exports.testSeparator = function (test) { test.showMenu(null, function (popup) { test.checkMenu([menu], [], []); - - // Get the menu element, makes sure it's OK. - let menuElt = test.getItemElt(popup, menu); - test.assert(menuElt, "Menu element should exist"); - - // Get the separator element inside the menu's popup, makes sure it's OK. - menuElt.open = true; - let sepElt = menuElt.firstChild.firstChild; - test.assert(sepElt, "Separator element should exist"); - test.assert(!sepElt.hidden, "Separator element should not be hidden"); - test.assertEqual(sepElt.localName, "menuseparator", - "Separator should be of expected type"); test.done(); }); }; @@ -1298,6 +1319,229 @@ exports.testSetLabelAfterShowOverflow = function (test) { }; +// Setting the label of an item in a Menu should work. +exports.testSetLabelMenuItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [loader.cm.Item({ label: "a" })] + }); + menu.items[0].label = "z"; + + test.assertEqual(menu.items[0].label, "z"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.addItem() should work. +exports.testMenuAddItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }) + ] + }); + menu.addItem(loader.cm.Item({ label: "item 1" })); + menu.addItem(loader.cm.Item({ label: "item 2" })); + + test.assertEqual(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + test.assertEqual(menu.items[i].label, "item " + i, + "item label should be correct"); + test.assertEqual(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding the same item twice to a menu should work as expected. +exports.testMenuAddItemTwice = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [] + }); + let subitem = loader.cm.Item({ label: "item 1" }) + menu.addItem(subitem); + menu.addItem(loader.cm.Item({ label: "item 0" })); + menu.addItem(subitem); + + test.assertEqual(menu.items.length, 2, + "menu should have correct number of items"); + for (let i = 0; i < 2; i++) { + test.assertEqual(menu.items[i].label, "item " + i, + "item label should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Menu.removeItem() should work. +exports.testMenuRemoveItem = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item 1" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "item 0" }), + subitem, + loader.cm.Item({ label: "item 2" }) + ] + }); + + // Removing twice should be harmless. + menu.removeItem(subitem); + menu.removeItem(subitem); + + test.assertEqual(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.assertEqual(menu.items.length, 2, + "menu should have correct number of items"); + test.assertEqual(menu.items[0].label, "item 0", + "item label should be correct"); + test.assertEqual(menu.items[1].label, "item 2", + "item label should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Adding an item currently contained in one menu to another menu should work. +exports.testMenuItemSwap = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu0 = loader.cm.Menu({ + label: "menu 0", + items: [subitem] + }); + let menu1 = loader.cm.Menu({ + label: "menu 1", + items: [] + }); + menu1.addItem(subitem); + + test.assertEqual(menu0.items.length, 0, + "menu should have correct number of items"); + + test.assertEqual(menu1.items.length, 1, + "menu should have correct number of items"); + test.assertEqual(menu1.items[0].label, "item", + "item label should be correct"); + + test.assertEqual(subitem.parentMenu, menu1, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu0, menu1], [], []); + test.done(); + }); +}; + + +// Destroying an item should remove it from its parent menu. +exports.testMenuItemDestroy = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let subitem = loader.cm.Item({ label: "item" }); + let menu = loader.cm.Menu({ + label: "menu", + items: [subitem] + }); + subitem.destroy(); + + test.assertEqual(menu.items.length, 0, + "menu should have correct number of items"); + test.assertEqual(subitem.parentMenu, null, + "item's parent menu should be correct"); + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Menu.items should work. +exports.testMenuItemsSetter = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let menu = loader.cm.Menu({ + label: "menu", + items: [ + loader.cm.Item({ label: "old item 0" }), + loader.cm.Item({ label: "old item 1" }) + ] + }); + menu.items = [ + loader.cm.Item({ label: "new item 0" }), + loader.cm.Item({ label: "new item 1" }), + loader.cm.Item({ label: "new item 2" }) + ]; + + test.assertEqual(menu.items.length, 3, + "menu should have correct number of items"); + for (let i = 0; i < 3; i++) { + test.assertEqual(menu.items[i].label, "new item " + i, + "item label should be correct"); + test.assertEqual(menu.items[i].parentMenu, menu, + "item's parent menu should be correct"); + } + + test.showMenu(null, function (popup) { + test.checkMenu([menu], [], []); + test.done(); + }); +}; + + +// Setting Item.data should work. +exports.testItemDataSetter = function (test) { + test = new TestHelper(test); + let loader = test.newLoader(); + + let item = loader.cm.Item({ label: "old item 0", data: "old" }); + item.data = "new"; + + test.assertEqual(item.data, "new", "item should have correct data"); + + test.showMenu(null, function (popup) { + test.checkMenu([item], [], []); + test.done(); + }); +}; + + // NO TESTS BELOW THIS LINE! /////////////////////////////////////////////////// // Run only a dummy test if context-menu doesn't support the host app. @@ -1375,11 +1619,34 @@ TestHelper.prototype = { // Asserts that elt, a DOM element representing item, looks OK. checkItemElt: function (elt, item) { - this.test.assertEqual(elt.getAttribute("label"), item.label, - "Item should have correct title"); - if (item.data) { - this.test.assertEqual(elt.getAttribute("value"), item.data, - "Item should have correct data"); + let itemType = this.getItemType(item); + + switch (itemType) { + case "Item": + this.test.assertEqual(elt.localName, "menuitem", + "Item DOM element should be a xul:menuitem"); + if (typeof(item.data) === "string") { + this.test.assertEqual(elt.getAttribute("value"), item.data, + "Item should have correct data"); + } + break + case "Menu": + this.test.assertEqual(elt.localName, "menu", + "Menu DOM element should be a xul:menu"); + let subPopup = elt.firstChild; + this.test.assert(subPopup, "xul:menu should have a child"); + this.test.assertEqual(subPopup.localName, "menupopup", + "xul:menu's first child should be a menupopup"); + break; + case "Separator": + this.test.assertEqual(elt.localName, "menuseparator", + "Separator DOM element should be a xul:menuseparator"); + break; + } + + if (itemType === "Item" || itemType === "Menu") { + this.test.assertEqual(elt.getAttribute("label"), item.label, + "Item should have correct title"); } }, @@ -1420,28 +1687,40 @@ TestHelper.prototype = { // Asserts that the items that are present in the menu because they match the // current context look OK. checkPresentItems: function (presentItems) { - for (let i = 0; i < presentItems.length; i++) { - let item = presentItems[i]; - let elt = this.getItemElt(this.contextMenuPopup, item); - - if (this.shouldOverflow(presentItems)) { - this.test.assert(!elt || elt.hidden, - "Item should not be present in top-level menu"); - - let overflowPopup = this.overflowPopup; - this.test.assert(overflowPopup, "Overflow submenu should be present"); - - elt = this.getItemElt(overflowPopup, item); - this.test.assert(elt && !elt.hidden, - "Item should be present in overflow submenu"); - } - else { - this.test.assert(elt && !elt.hidden, - "Item should be present in top-level menu"); - } + function recurse(popup, items, isTopLevel) { + items.forEach(function (item) { + let elt = this.getItemElt(popup, item); + + if (isTopLevel) { + if (this.shouldOverflow(items)) { + this.test.assert(!elt || elt.hidden, + "Item should not be present in top-level menu"); + + let overflowPopup = this.overflowPopup; + this.test.assert(overflowPopup, + "Overflow submenu should be present"); + + elt = this.getItemElt(overflowPopup, item); + this.test.assert(elt && !elt.hidden, + "Item should be present in overflow submenu"); + } + else { + this.test.assert(elt && !elt.hidden, + "Item should be present in top-level menu"); + } + } + else { + this.test.assert(elt && !elt.hidden, + "Item should be present in menu"); + } - this.checkItemElt(elt, item); + this.checkItemElt(elt, item); + if (this.getItemType(item) === "Menu") + recurse.call(this, elt.firstChild, item.items, false); + }, this); } + + recurse.call(this, this.contextMenuPopup, presentItems, true); }, // Asserts that items that have been removed from the menu are really removed. @@ -1530,19 +1809,26 @@ TestHelper.prototype = { } while (this.loaders.length) { let browserManager = this.loaders[0].globalScope.browserManager; - let items = browserManager.items.slice(); + let topLevelItems = browserManager.topLevelItems.slice(); let privatePropsKey = this.loaders[0].globalScope.PRIVATE_PROPS_KEY; + let workerRegs = topLevelItems.map(function (item) { + return item.valueOf(privatePropsKey)._workerReg; + }); + this.loaders[0].unload(); // Make sure the browser manager is cleaned up. - this.test.assertEqual(browserManager.windows.length, 0, + this.test.assertEqual(browserManager.browserWins.length, 0, "browserManager should have no windows left"); - this.test.assertEqual(browserManager.items.length, 0, + this.test.assertEqual(browserManager.topLevelItems.length, 0, "browserManager should have no items left"); // Make sure the items' worker registries are cleaned up. - items.forEach(function (item) { - let workerReg = item.valueOf(privatePropsKey).workerReg; + topLevelItems.forEach(function (item) { + this.test.assert(!("_workerReg" in item.valueOf(privatePropsKey)), + "item's worker registry should be removed"); + }, this); + workerRegs.forEach(function (workerReg) { this.test.assertEqual(Object.keys(workerReg.winWorkers).length, 0, "worker registry should be empty"); this.test.assertEqual( @@ -1583,12 +1869,25 @@ TestHelper.prototype = { getItemElt: function (popup, item) { let nodes = popup.childNodes; for (let i = nodes.length - 1; i >= 0; i--) { - if (nodes[i].getAttribute("label") === item.label) + if (this.getItemType(item) === "Separator") { + if (nodes[i].localName === "menuseparator") + return nodes[i]; + } + else if (nodes[i].getAttribute("label") === item.label) return nodes[i]; } return null; }, + // Returns "Item", "Menu", or "Separator". + getItemType: function (item) { + // Could use instanceof here, but that would require accessing the loader + // that created the item, and I don't want to A) somehow search through the + // this.loaders list to find it, and B) assume there are any live loaders at + // all. + return /^\[object (Item|Menu|Separator)/.exec(item.toString())[1]; + }, + // Returns a wrapper around a new loader: { loader, cm, unload, globalScope }. // loader is a Cuddlefish sandboxed loader, cm is the context menu module, // globalScope is the context menu module's global scope, and unload is a diff --git a/packages/api-utils/lib/cuddlefish.js b/packages/api-utils/lib/cuddlefish.js index fa93914fe..2134dc0e7 100644 --- a/packages/api-utils/lib/cuddlefish.js +++ b/packages/api-utils/lib/cuddlefish.js @@ -84,8 +84,8 @@ shims.filename = shimsPath; - function unloadLoader(reason) { - this.require("unload").send(reason); + function unloadLoader(reason, onError) { + this.require("unload").send(reason, onError); } function makeGetModuleExports(delegate) { diff --git a/packages/api-utils/lib/unload.js b/packages/api-utils/lib/unload.js index 46574d414..3bbeb389b 100644 --- a/packages/api-utils/lib/unload.js +++ b/packages/api-utils/lib/unload.js @@ -11,12 +11,13 @@ var when = exports.when = function when(observer) { observers.unshift(observer); }; -var send = exports.send = function send(reason) { +var send = exports.send = function send(reason, onError) { + onError = onError || console.exception; observers.forEach(function (observer) { try { observer(reason); } catch (e) { - console.exception(e); + onError(e); } }); }; diff --git a/packages/api-utils/tests/test-unload.js b/packages/api-utils/tests/test-unload.js index 94c228704..2f90b43b2 100644 --- a/packages/api-utils/tests/test-unload.js +++ b/packages/api-utils/tests/test-unload.js @@ -45,9 +45,10 @@ exports.testUnloading = function(test) { var loader = test.makeSandboxedLoader(); var ul = loader.require("unload"); var unloadCalled = 0; + var errorsReported = 0; function unload() { unloadCalled++; - throw "error"; + throw new Error("error"); } ul.when(unload); @@ -56,9 +57,11 @@ exports.testUnloading = function(test) { function unload2() { unloadCalled++; } ul.when(unload2); - loader.unload(); + loader.unload(undefined, function onError() { errorsReported++; }); test.assertEqual(unloadCalled, 2, "Unloader functions are called on unload."); + test.assertEqual(errorsReported, 1, + "One unload handler threw exception"); }; exports.testEnsure = function(test) { diff --git a/python-lib/cuddlefish/runner.py b/python-lib/cuddlefish/runner.py index 92584093e..04287da34 100644 --- a/python-lib/cuddlefish/runner.py +++ b/python-lib/cuddlefish/runner.py @@ -320,7 +320,7 @@ def maybe_remove_logfile(): print >>sys.stderr, "Using binary at '%s'." % runner.binary # ensure running Firefox 4.0+ - version_output = check_output([runner.binary, "-v"]) + version_output = check_output(runner.command + ["-v"]) mo = re.search(r"Mozilla (Firefox|Iceweasel) (\d+)\.[\d\.]+", version_output) if not mo: diff --git a/static-files/md/dev-guide/addon-development/installation.md b/static-files/md/dev-guide/addon-development/installation.md index 9a56d9b8c..3504ee030 100644 --- a/static-files/md/dev-guide/addon-development/installation.md +++ b/static-files/md/dev-guide/addon-development/installation.md @@ -1,53 +1,92 @@ # Installation # +## Prerequisites + To develop with the Add-on SDK, you'll need: * [Python](http://www.python.org/) 2.5 or 2.6. Note that versions 3.0 and 3.1 of Python are not supported. Make sure that Python is in your path. -* A version of Firefox that uses Gecko 2.0.0.7 or later - (for example, Firefox 4.0). +* Firefox version 4.0 or later. -At the moment, the latest stable version of the Add-on SDK is 1.0b5. +At the moment, the latest stable version of the Add-on SDK is 1.0. You can obtain it as a [tarball](https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/jetpack-sdk-latest.tar.gz) or a [zip file](https://ftp.mozilla.org/pub/mozilla.org/labs/jetpack/jetpack-sdk-latest.zip). Alternatively, you can get the latest development version of the Add-on SDK from its [GitHub repository](https://github.com/mozilla/addon-sdk). +## Installation on Mac OS X / Linux ## + Extract the file contents wherever you choose, and navigate to the root directory of the SDK with a shell/command prompt. For example:
-  ~/mozilla > tar -xf addon-sdk-1.0b5.tar.gz
-  ~/mozilla > cd addon-sdk-1.0b5
-  ~/mozilla/addon-sdk-1.0b5 >
+  ~/mozilla > tar -xf addon-sdk-1.0.tar.gz
+  ~/mozilla > cd addon-sdk-1.0
+  ~/mozilla/addon-sdk-1.0 >
+
+ +Then run: + +
+  ~/mozilla/addon-sdk-1.0 > source bin/activate
 
-Then, if you're on Linux, OS X, or another Unix-based system, run: +Your command prompt should now have a new prefix containing the name of the +SDK's root directory:
-  ~/mozilla/addon-sdk-1.0b5 > source bin/activate
+  (addon-sdk-1.0)~/mozilla/addon-sdk-1.0 >
 
-Otherwise, if you're on Windows, run: +## Installation on Windows ## + +Extract the file contents wherever you choose, and navigate to the root +directory of the SDK with a shell/command prompt. For example:
-  C:\Users\Mozilla\addon-sdk-1.0b5> bin\activate
+  C:\Users\mozilla\sdk>7z.exe x addon-sdk-1.0.zip
+  C:\Users\mozilla\sdk>cd addon-sdk-1.0
+  C:\Users\mozilla\sdk\addon-sdk-1.0>
 
-Now the beginning of your command prompt should contain the text -`(addon-sdk)`, which means that your shell has entered a special -virtual environment that gives you access to the Add-on SDK's -command-line tools. +Then run: + +
+  C:\Users\mozilla\sdk\addon-sdk-1.0>bin\activate
+
+ +You might see an error like this: + +
+  ERROR: The system was unable to find the specified registry key or value.
+
+ +This is a known issue, being tracked as +[bug 574563](https://bugzilla.mozilla.org/show_bug.cgi?id=574563), and should +not affect the proper functioning of the SDK at all. + +Your command prompt should now have a new prefix containing the full path to +the SDK's root directory: + +
+  (C:\Users\mozilla\sdk\addon-sdk-1.0) C:\Users\Work\sdk\addon-sdk-1.0>
+
+ +## SDK Virtual Environment ## + +The new prefix to your command prompt indicates that your shell has entered +a virtual environment that gives you access to the Add-on SDK's command-line +tools. -At any time, you can leave a virtual environment by running -`deactivate`. +At any time, you can leave a virtual environment by running `deactivate`. The virtual environment is specific to this particular command prompt. If you close this command prompt, it is deactivated and you need to type -`source bin/activate` in a new command prompt to reactivate it. If -you open a new command prompt, the SDK will not be active in the new prompt. +`source bin/activate` or `bin\activate` in a new command prompt to reactivate +it. If you open a new command prompt, the SDK will not be active in the new +prompt. You can have multiple copies of the SDK in different locations on disk and switch between them, or even have them both activated in different command @@ -58,7 +97,7 @@ prompts at the same time. Run this at your shell prompt:
-  ~/mozilla/addon-sdk-1.0b5 > cfx
+  ~/mozilla/addon-sdk-1.0 > cfx
 
It should produce output whose first line looks something like this, followed by