diff --git a/browser/devtools/highlighter/inspector.jsm b/browser/devtools/highlighter/inspector.jsm index 3008e03b6f83..3734298e3619 100644 --- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -18,6 +18,7 @@ Cu.import("resource:///modules/devtools/MarkupView.jsm"); Cu.import("resource:///modules/highlighter.jsm"); Cu.import("resource:///modules/devtools/LayoutView.jsm"); Cu.import("resource:///modules/devtools/LayoutHelpers.jsm"); +Cu.import("resource:///modules/devtools/EventEmitter.jsm"); // Inspector notifications dispatched through the nsIObserverService. const INSPECTOR_NOTIFICATIONS = { @@ -67,7 +68,7 @@ function Inspector(aIUI) this._IUI = aIUI; this._winID = aIUI.winID; this._browser = aIUI.browser; - this._listeners = {}; + this._eventEmitter = new EventEmitter(); this._browser.addEventListener("resize", this, true); @@ -147,7 +148,7 @@ Inspector.prototype = { this._destroyMarkup(); this._browser.removeEventListener("resize", this, true); delete this._IUI; - delete this._listeners; + delete this._eventEmitter; }, /** @@ -284,7 +285,7 @@ Inspector.prototype = { this._markupBox.removeAttribute("hidden"); this.markup = new MarkupView(this, this._markupFrame); - this._emit("markuploaded"); + this.emit("markuploaded"); }, _destroyMarkup: function Inspector__destroyMarkup() @@ -348,8 +349,7 @@ Inspector.prototype = { delete this._frozen; }, - /// Event stuff. Would like to refactor this eventually. - /// Emulates the jetpack event source, which has a nice API. + /// Forward the events related calls to the event emitter. /** * Connect a listener to this object. @@ -361,10 +361,7 @@ Inspector.prototype = { */ on: function Inspector_on(aEvent, aListener) { - if (!(aEvent in this._listeners)) { - this._listeners[aEvent] = []; - } - this._listeners[aEvent].push(aListener); + this._eventEmitter.on(aEvent, aListener); }, /** @@ -377,11 +374,7 @@ Inspector.prototype = { */ once: function Inspector_once(aEvent, aListener) { - let handler = function() { - this.removeListener(aEvent, handler); - aListener(); - }.bind(this); - this.on(aEvent, handler); + this._eventEmitter.once(aEvent, aListener); }, /** @@ -393,35 +386,18 @@ Inspector.prototype = { * @param function aListener * The listener to remove. */ - removeListener: function Inspector_removeListener(aEvent, aListener) + off: function Inspector_removeListener(aEvent, aListener) { - this._listeners[aEvent] = this._listeners[aEvent].filter(function(l) aListener != l); + this._eventEmitter.off(aEvent, aListener); }, /** * Emit an event on the inspector. All arguments to this method will * be sent to listner functions. */ - _emit: function Inspector__emit(aEvent) + emit: function Inspector_emit() { - if (!(aEvent in this._listeners)) - return; - - let originalListeners = this._listeners[aEvent]; - for (let listener of this._listeners[aEvent]) { - // If the inspector was destroyed during event emission, stop - // emitting. - if (!this._listeners) { - break; - } - - // If listeners were removed during emission, make sure the - // event handler we're going to fire wasn't removed. - if (originalListeners === this._listeners[aEvent] || - this._listeners[aEvent].some(function(l) l === listener)) { - listener.apply(null, arguments); - } - } + this._eventEmitter.emit.apply(this._eventEmitter, arguments); } } @@ -872,13 +848,13 @@ InspectorUI.prototype = { this.inspecting = true; this.highlighter.unlock(); this._notifySelected(); - this._currentInspector._emit("unlocked"); + this._currentInspector.emit("unlocked"); }, _notifySelected: function IUI__notifySelected(aFrom) { this._currentInspector._cancelLayoutChange(); - this._currentInspector._emit("select", aFrom); + this._currentInspector.emit("select", aFrom); }, /** @@ -908,7 +884,7 @@ InspectorUI.prototype = { this.highlighter.lock(); this._notifySelected(); - this._currentInspector._emit("locked"); + this._currentInspector.emit("locked"); }, /** @@ -993,7 +969,7 @@ InspectorUI.prototype = { this.highlighter.updateInfobar(); this.highlighter.invalidateSize(); this.breadcrumbs.updateSelectors(); - this._currentInspector._emit("change", aUpdater); + this._currentInspector.emit("change", aUpdater); }, ///////////////////////////////////////////////////////////////////////// @@ -1821,8 +1797,8 @@ InspectorStyleSidebar.prototype = { // If the current tool is already loaded, notify that we're // showing this sidebar. if (aTool.loaded) { - this._inspector._emit("sidebaractivated", aTool.id); - this._inspector._emit("sidebaractivated-" + aTool.id); + this._inspector.emit("sidebaractivated", aTool.id); + this._inspector.emit("sidebaractivated-" + aTool.id); return; } @@ -1841,14 +1817,14 @@ InspectorStyleSidebar.prototype = { aTool.loaded = true; aTool.context = aTool.registration.load(this._inspector, aTool.frame); - this._inspector._emit("sidebaractivated", aTool.id); + this._inspector.emit("sidebaractivated", aTool.id); // Send an event specific to the activation of this panel. For // this initial event, include a "createpanel" argument // to let panels watch sidebaractivated to refresh themselves // but ignore the one immediately after their load. // I don't really like this, we should find a better solution. - this._inspector._emit("sidebaractivated-" + aTool.id, "createpanel"); + this._inspector.emit("sidebaractivated-" + aTool.id, "createpanel"); }.bind(this); aTool.frame.addEventListener("load", aTool.onLoad, true); aTool.frame.setAttribute("src", aTool.registration.contentURL); diff --git a/browser/devtools/layoutview/LayoutView.jsm b/browser/devtools/layoutview/LayoutView.jsm index a154000f070d..189b74c170b8 100644 --- a/browser/devtools/layoutview/LayoutView.jsm +++ b/browser/devtools/layoutview/LayoutView.jsm @@ -123,8 +123,8 @@ LayoutView.prototype = { * Destroy the nodes. Remove listeners. */ destroy: function LV_destroy() { - this.inspector.removeListener("select", this.onSelect); - this.inspector.removeListener("unlocked", this.onUnlock); + this.inspector.off("select", this.onSelect); + this.inspector.off("unlocked", this.onUnlock); this.browser.removeEventListener("MozAfterPaint", this.update, true); this.iframe.removeEventListener("keypress", this.bound_handleKeypress, true); this.inspector.chromeWindow.removeEventListener("message", this.onMessage, true); diff --git a/browser/devtools/markupview/MarkupView.jsm b/browser/devtools/markupview/MarkupView.jsm index 600c0ff10a75..eaee1a9151f4 100644 --- a/browser/devtools/markupview/MarkupView.jsm +++ b/browser/devtools/markupview/MarkupView.jsm @@ -331,7 +331,7 @@ MarkupView.prototype = { this._updateChildren(container); } } - this._inspector._emit("markupmutation"); + this._inspector.emit("markupmutation"); }, /** @@ -479,7 +479,7 @@ MarkupView.prototype = { this._frame.removeEventListener("keydown", this._boundKeyDown, true); delete this._boundKeyDown; - this._inspector.removeListener("select", this._boundSelect); + this._inspector.off("select", this._boundSelect); delete this._boundSelect; delete this._elt; diff --git a/browser/devtools/shared/EventEmitter.jsm b/browser/devtools/shared/EventEmitter.jsm new file mode 100644 index 000000000000..dafa50c9a748 --- /dev/null +++ b/browser/devtools/shared/EventEmitter.jsm @@ -0,0 +1,80 @@ +var EXPORTED_SYMBOLS = ["EventEmitter"]; + +function EventEmitter() { +} + +EventEmitter.prototype = { + /** + * Connect a listener. + * + * @param string aEvent + * The event name to which we're connecting. + * @param function aListener + * Called when the event is fired. + */ + on: function EventEmitter_on(aEvent, aListener) { + if (!this._eventEmitterListeners) + this._eventEmitterListeners = new Map(); + if (!this._eventEmitterListeners.has(aEvent)) { + this._eventEmitterListeners.set(aEvent, []); + } + this._eventEmitterListeners.get(aEvent).push(aListener); + }, + + /** + * Listen for the next time an event is fired. + * + * @param string aEvent + * The event name to which we're connecting. + * @param function aListener + * Called when the event is fired. Will be called at most one time. + */ + once: function EventEmitter_once(aEvent, aListener) { + let handler = function() { + this.off(aEvent, handler); + aListener(); + }.bind(this); + this.on(aEvent, handler); + }, + + /** + * Remove a previously-registered event listener. Works for events + * registered with either on or once. + * + * @param string aEvent + * The event name whose listener we're disconnecting. + * @param function aListener + * The listener to remove. + */ + off: function EventEmitter_off(aEvent, aListener) { + if (!this._eventEmitterListeners) + return; + let listeners = this._eventEmitterListeners.get(aEvent); + this._eventEmitterListeners.set(aEvent, listeners.filter(function(l) aListener != l)); + }, + + /** + * Emit an event. All arguments to this method will + * be sent to listner functions. + */ + emit: function EventEmitter_emit(aEvent) { + if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent)) + return; + + let originalListeners = this._eventEmitterListeners.get(aEvent); + for (let listener of this._eventEmitterListeners.get(aEvent)) { + // If the object was destroyed during event emission, stop + // emitting. + if (!this._eventEmitterListeners) { + break; + } + + // If listeners were removed during emission, make sure the + // event handler we're going to fire wasn't removed. + if (originalListeners === this._eventEmitterListeners.get(aEvent) || + this._eventEmitterListeners.get(aEvent).some(function(l) l === listener)) { + listener.apply(null, arguments); + } + } + }, +} diff --git a/browser/devtools/shared/test/Makefile.in b/browser/devtools/shared/test/Makefile.in index f45dd2cb41f9..d7b39ec57e9d 100644 --- a/browser/devtools/shared/test/Makefile.in +++ b/browser/devtools/shared/test/Makefile.in @@ -22,6 +22,7 @@ MOCHITEST_BROWSER_FILES = \ browser_toolbar_tooltip.js \ browser_toolbar_webconsole_errors_count.js \ browser_layoutHelpers.js \ + browser_eventemitter_basic.js \ head.js \ helpers.js \ leakhunt.js \ diff --git a/browser/devtools/shared/test/browser_eventemitter_basic.js b/browser/devtools/shared/test/browser_eventemitter_basic.js new file mode 100644 index 000000000000..c54ce5420eef --- /dev/null +++ b/browser/devtools/shared/test/browser_eventemitter_basic.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + + +function test() { + Cu.import("resource:///modules/devtools/EventEmitter.jsm", this); + let emitter = new EventEmitter(); + ok(emitter, "We have an event emitter"); + + emitter.on("next", next); + emitter.emit("next", "abc", "def"); + + let beenHere1 = false; + function next(eventName, str1, str2) { + is(eventName, "next", "Got event"); + is(str1, "abc", "Argument 1 is correct"); + is(str2, "def", "Argument 2 is correct"); + + ok(!beenHere1, "first time in next callback"); + beenHere1 = true; + + emitter.off("next", next); + + emitter.emit("next"); + + emitter.once("onlyonce", onlyOnce); + + emitter.emit("onlyonce"); + emitter.emit("onlyonce"); + } + + let beenHere2 = false; + function onlyOnce() { + ok(!beenHere2, "\"once\" listner has been called once"); + beenHere2 = true; + emitter.emit("onlyonce"); + + killItWhileEmitting(); + } + + function killItWhileEmitting() { + function c1() { + ok(true, "c1 called"); + } + function c2() { + ok(true, "c2 called"); + emitter.off("tick", c3); + } + function c3() { + ok(false, "c3 should not be called"); + } + function c4() { + ok(true, "c4 called"); + } + + emitter.on("tick", c1); + emitter.on("tick", c2); + emitter.on("tick", c3); + emitter.on("tick", c4); + + emitter.emit("tick"); + + delete emitter; + finish(); + } +} diff --git a/browser/devtools/styleinspector/StyleInspector.jsm b/browser/devtools/styleinspector/StyleInspector.jsm index a7e4a4d0119c..673d88afe5a1 100644 --- a/browser/devtools/styleinspector/StyleInspector.jsm +++ b/browser/devtools/styleinspector/StyleInspector.jsm @@ -146,9 +146,9 @@ RuleViewTool.prototype = { }, destroy: function RVT_destroy() { - this.inspector.removeListener("select", this._onSelect); - this.inspector.removeListener("change", this._onChange); - this.inspector.removeListener("sidebaractivated-ruleview", this._onChange); + this.inspector.off("select", this._onSelect); + this.inspector.off("change", this._onChange); + this.inspector.off("sidebaractivated-ruleview", this._onChange); this.view.element.removeEventListener("CssRuleViewChanged", this._changeHandler); this.view.element.removeEventListener("CssRuleViewCSSLinkClicked", @@ -214,9 +214,9 @@ ComputedViewTool.prototype = { destroy: function CVT_destroy(aContext) { - this.inspector.removeListener("select", this._onSelect); - this.inspector.removeListener("change", this._onChange); - this.inspector.removeListener("sidebaractivated-computedview", this._onChange); + this.inspector.off("select", this._onSelect); + this.inspector.off("change", this._onChange); + this.inspector.off("sidebaractivated-computedview", this._onChange); this.view.destroy(); delete this.view;