OLD | NEW |
---|---|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 var eventBindingsNatives = requireNative('event_bindings'); | 5 var eventBindingsNatives = requireNative('event_bindings'); |
6 var AttachEvent = eventBindingsNatives.AttachEvent; | 6 var AttachEvent = eventBindingsNatives.AttachEvent; |
7 var DetachEvent = eventBindingsNatives.DetachEvent; | 7 var DetachEvent = eventBindingsNatives.DetachEvent; |
8 var AttachFilteredEvent = eventBindingsNatives.AttachFilteredEvent; | |
9 var DetachFilteredEvent = eventBindingsNatives.DetachFilteredEvent; | |
10 var MatchAgainstEventFilter = eventBindingsNatives.MatchAgainstEventFilter; | |
8 var sendRequest = require('sendRequest').sendRequest; | 11 var sendRequest = require('sendRequest').sendRequest; |
9 var utils = require('utils'); | 12 var utils = require('utils'); |
10 | 13 |
11 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); | 14 var chromeHidden = requireNative('chrome_hidden').GetChromeHidden(); |
12 var GetExtensionAPIDefinition = | 15 var GetExtensionAPIDefinition = |
13 requireNative('apiDefinitions').GetExtensionAPIDefinition; | 16 requireNative('apiDefinitions').GetExtensionAPIDefinition; |
14 | 17 |
15 // Schemas for the rule-style functions on the events API that | 18 // Schemas for the rule-style functions on the events API that |
16 // only need to be generated occasionally, so populate them lazily. | 19 // only need to be generated occasionally, so populate them lazily. |
17 var ruleFunctionSchemas = { | 20 var ruleFunctionSchemas = { |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
67 $Array.prototype.toJSON = customizedArrayToJSON; | 70 $Array.prototype.toJSON = customizedArrayToJSON; |
68 } | 71 } |
69 } | 72 } |
70 }; | 73 }; |
71 | 74 |
72 this.parse = function(thing) { | 75 this.parse = function(thing) { |
73 return $jsonParse(thing); | 76 return $jsonParse(thing); |
74 }; | 77 }; |
75 })(); | 78 })(); |
76 | 79 |
80 // Handles adding/removing/dispatching listeners for unfiltered events. | |
81 var UnfilteredAttachmentStrategy = function(event) { | |
Matt Perry
2012/06/13 01:24:27
How much overhead would it be if we just treated u
koz (OOO until 15th September)
2012/06/14 02:15:55
Yeah, it would. I don't think the overhead would b
Matt Perry
2012/06/14 18:48:56
Could EventFilter skip rebuilding if there are no
koz (OOO until 15th September)
2012/06/15 00:07:14
Yes, that could be done. It would require making t
Matt Perry
2012/06/15 00:10:40
That's a good point. I'm convinced.
| |
82 this.event_ = event; | |
83 }; | |
84 | |
85 UnfilteredAttachmentStrategy.prototype.onAddedListener = | |
86 function(listener) { | |
87 // Only attach / detach on the first / last listener removed. | |
88 if (this.event_.listeners_.length == 0) | |
89 AttachEvent(this.event_.eventName_); | |
90 }; | |
91 | |
92 UnfilteredAttachmentStrategy.prototype.onRemovedListener = | |
93 function(listener) { | |
94 if (this.event_.listeners_.length == 0) | |
95 this.detach(true); | |
96 }; | |
97 | |
98 UnfilteredAttachmentStrategy.prototype.detach = function(manual) { | |
99 var i = allAttachedEvents.indexOf(this.event_); | |
100 if (i >= 0) | |
101 delete allAttachedEvents[i]; | |
102 DetachEvent(this.event_.eventName_, manual); | |
103 }; | |
104 | |
105 UnfilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
106 return this.event_.listeners_; | |
107 }; | |
108 | |
109 var FilteredAttachmentStrategy = function(event) { | |
110 this.event_ = event; | |
111 this.listenerMap_ = {}; | |
112 }; | |
113 | |
114 FilteredAttachmentStrategy.idToEventMap = {}; | |
115 | |
116 FilteredAttachmentStrategy.prototype.onAddedListener = function(listener) { | |
117 var id = AttachFilteredEvent(this.event_.eventName_, | |
118 listener.filters || {}); | |
119 listener.id = id; | |
120 this.listenerMap_[id] = listener; | |
121 FilteredAttachmentStrategy.idToEventMap[id] = this.event_; | |
122 }; | |
123 | |
124 FilteredAttachmentStrategy.prototype.onRemovedListener = function(listener) { | |
125 this.detachListener(listener, true); | |
126 }; | |
127 | |
128 FilteredAttachmentStrategy.prototype.detachListener = | |
129 function(listener, manual) { | |
130 if (listener.id == undefined) | |
131 throw new Error("listener.id undefined - '" + listener + "'"); | |
132 var id = listener.id; | |
133 delete this.listenerMap_[id]; | |
134 delete FilteredAttachmentStrategy.idToEventMap[id]; | |
135 DetachFilteredEvent(id, manual); | |
136 }; | |
137 | |
138 FilteredAttachmentStrategy.prototype.detach = function(manual) { | |
139 for (var i in this.listenerMap_) | |
140 this.detachListener(this.listenerMap_[i], manual); | |
141 }; | |
142 | |
143 FilteredAttachmentStrategy.prototype.getListenersByIDs = function(ids) { | |
144 console.log('FilteredAttachmentStrategy.getListenersByIDs(' + ids + ')'); | |
145 var result = []; | |
146 for (var i = 0; i < ids.length; i++) | |
147 result.push(this.listenerMap_[ids[i]]); | |
148 return result; | |
149 }; | |
150 | |
77 // Event object. If opt_eventName is provided, this object represents | 151 // Event object. If opt_eventName is provided, this object represents |
78 // the unique instance of that named event, and dispatching an event | 152 // the unique instance of that named event, and dispatching an event |
79 // with that name will route through this object's listeners. Note that | 153 // with that name will route through this object's listeners. Note that |
80 // opt_eventName is required for events that support rules. | 154 // opt_eventName is required for events that support rules. |
81 // | 155 // |
82 // Example: | 156 // Example: |
83 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); | 157 // chrome.tabs.onChanged = new chrome.Event("tab-changed"); |
84 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); | 158 // chrome.tabs.onChanged.addListener(function(data) { alert(data); }); |
85 // chromeHidden.Event.dispatch("tab-changed", "hi"); | 159 // chromeHidden.Event.dispatch("tab-changed", "hi"); |
86 // will result in an alert dialog that says 'hi'. | 160 // will result in an alert dialog that says 'hi'. |
87 // | 161 // |
88 // If opt_eventOptions exists, it is a dictionary that contains the boolean | 162 // If opt_eventOptions exists, it is a dictionary that contains the boolean |
89 // entries "supportsListeners" and "supportsRules". | 163 // entries "supportsListeners" and "supportsRules". |
90 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { | 164 chrome.Event = function(opt_eventName, opt_argSchemas, opt_eventOptions) { |
91 this.eventName_ = opt_eventName; | 165 this.eventName_ = opt_eventName; |
92 this.listeners_ = []; | 166 this.listeners_ = []; |
93 this.eventOptions_ = opt_eventOptions || | 167 this.eventOptions_ = opt_eventOptions || |
94 {"supportsListeners": true, "supportsRules": false}; | 168 {supportsFilters: false, |
169 supportsListeners: true, | |
170 supportsRules: false, | |
171 }; | |
95 | 172 |
96 if (this.eventOptions_.supportsRules && !opt_eventName) | 173 if (this.eventOptions_.supportsRules && !opt_eventName) |
97 throw new Error("Events that support rules require an event name."); | 174 throw new Error("Events that support rules require an event name."); |
98 | 175 |
176 if (this.eventOptions_.supportsFilters) { | |
177 this.attachmentStrategy_ = new FilteredAttachmentStrategy(this); | |
178 } else { | |
179 this.attachmentStrategy_ = new UnfilteredAttachmentStrategy(this); | |
180 } | |
181 | |
99 // Validate event arguments (the data that is passed to the callbacks) | 182 // Validate event arguments (the data that is passed to the callbacks) |
100 // if we are in debug. | 183 // if we are in debug. |
101 if (opt_argSchemas && | 184 if (opt_argSchemas && |
102 chromeHidden.validateCallbacks && | 185 chromeHidden.validateCallbacks && |
103 chromeHidden.validate) { | 186 chromeHidden.validate) { |
104 | 187 |
105 this.validateEventArgs_ = function(args) { | 188 this.validateEventArgs_ = function(args) { |
106 try { | 189 try { |
107 chromeHidden.validate(args, opt_argSchemas); | 190 chromeHidden.validate(args, opt_argSchemas); |
108 } catch (exception) { | 191 } catch (exception) { |
(...skipping 20 matching lines...) Expand all Loading... | |
129 | 212 |
130 chromeHidden.Event.registerArgumentMassager = function(name, fn) { | 213 chromeHidden.Event.registerArgumentMassager = function(name, fn) { |
131 if (eventArgumentMassagers[name]) | 214 if (eventArgumentMassagers[name]) |
132 throw new Error("Massager already registered for event: " + name); | 215 throw new Error("Massager already registered for event: " + name); |
133 eventArgumentMassagers[name] = fn; | 216 eventArgumentMassagers[name] = fn; |
134 }; | 217 }; |
135 | 218 |
136 // Dispatches a named event with the given JSON array, which is deserialized | 219 // Dispatches a named event with the given JSON array, which is deserialized |
137 // before dispatch. The JSON array is the list of arguments that will be | 220 // before dispatch. The JSON array is the list of arguments that will be |
138 // sent with the event callback. | 221 // sent with the event callback. |
139 chromeHidden.Event.dispatchJSON = function(name, args) { | 222 chromeHidden.Event.dispatchJSON = function(name, args, filteringInfo) { |
223 var listenerIDs; | |
224 | |
225 if (filteringInfo) { | |
226 filteringInfo = chromeHidden.JSON.parse(filteringInfo); | |
227 listenerIDs = MatchAgainstEventFilter(name, filteringInfo); | |
228 console.log('listenerIDs = ' + listenerIDs); | |
229 } | |
140 if (attachedNamedEvents[name]) { | 230 if (attachedNamedEvents[name]) { |
141 if (args) { | 231 if (args) { |
142 args = chromeHidden.JSON.parse(args); | 232 args = chromeHidden.JSON.parse(args); |
143 if (eventArgumentMassagers[name]) | 233 if (eventArgumentMassagers[name]) |
144 eventArgumentMassagers[name](args); | 234 eventArgumentMassagers[name](args); |
145 } | 235 } |
146 var result = attachedNamedEvents[name].dispatch.apply( | 236 var result = attachedNamedEvents[name].dispatch_(args, listenerIDs); |
147 attachedNamedEvents[name], args); | |
148 if (result && result.validationErrors) | 237 if (result && result.validationErrors) |
149 return result.validationErrors; | 238 return result.validationErrors; |
150 } | 239 } |
151 }; | 240 }; |
152 | 241 |
153 // Dispatches a named event with the given arguments, supplied as an array. | 242 // Dispatches a named event with the given arguments, supplied as an array. |
154 chromeHidden.Event.dispatch = function(name, args) { | 243 chromeHidden.Event.dispatch = function(name, args) { |
155 if (attachedNamedEvents[name]) { | 244 if (attachedNamedEvents[name]) { |
156 attachedNamedEvents[name].dispatch.apply( | 245 attachedNamedEvents[name].dispatch.apply( |
157 attachedNamedEvents[name], args); | 246 attachedNamedEvents[name], args); |
158 } | 247 } |
159 }; | 248 }; |
160 | 249 |
161 // Test if a named event has any listeners. | 250 // Test if a named event has any listeners. |
162 chromeHidden.Event.hasListener = function(name) { | 251 chromeHidden.Event.hasListener = function(name) { |
163 return (attachedNamedEvents[name] && | 252 return (attachedNamedEvents[name] && |
164 attachedNamedEvents[name].listeners_.length > 0); | 253 attachedNamedEvents[name].listeners_.length > 0); |
165 }; | 254 }; |
166 | 255 |
167 // Registers a callback to be called when this event is dispatched. | 256 // Registers a callback to be called when this event is dispatched. |
168 chrome.Event.prototype.addListener = function(cb) { | 257 chrome.Event.prototype.addListener = function(cb, filters) { |
169 if (!this.eventOptions_.supportsListeners) | 258 if (!this.eventOptions_.supportsListeners) |
170 throw new Error("This event does not support listeners."); | 259 throw new Error("This event does not support listeners."); |
260 if (filters) { | |
261 if (!this.eventOptions_.supportsFilters) | |
262 throw new Error("This event does not support filters."); | |
263 if (filters.url && !(filters.url instanceof Array)) | |
264 throw new Error("filters.url should be an array"); | |
265 } | |
266 var listener = {callback: cb, filters: filters}; | |
267 this.attach_(listener); | |
268 this.listeners_.push(listener); | |
269 }; | |
270 | |
271 chrome.Event.prototype.attach_ = function(listener) { | |
272 this.attachmentStrategy_.onAddedListener(listener); | |
171 if (this.listeners_.length == 0) { | 273 if (this.listeners_.length == 0) { |
172 this.attach_(); | 274 allAttachedEvents[allAttachedEvents.length] = this; |
275 if (!this.eventName_) | |
276 return; | |
277 | |
278 if (attachedNamedEvents[this.eventName_]) { | |
279 throw new Error("chrome.Event '" + this.eventName_ + | |
280 "' is already attached."); | |
281 } | |
282 | |
283 attachedNamedEvents[this.eventName_] = this; | |
173 } | 284 } |
174 this.listeners_.push(cb); | |
175 }; | 285 }; |
176 | 286 |
177 // Unregisters a callback. | 287 // Unregisters a callback. |
178 chrome.Event.prototype.removeListener = function(cb) { | 288 chrome.Event.prototype.removeListener = function(cb) { |
179 if (!this.eventOptions_.supportsListeners) | 289 if (!this.eventOptions_.supportsListeners) |
180 throw new Error("This event does not support listeners."); | 290 throw new Error("This event does not support listeners."); |
181 var idx = this.findListener_(cb); | 291 var idx = this.findListener_(cb); |
182 if (idx == -1) { | 292 if (idx == -1) { |
183 return; | 293 return; |
184 } | 294 } |
185 | 295 |
186 this.listeners_.splice(idx, 1); | 296 var removedListener = this.listeners_.splice(idx, 1)[0]; |
297 this.attachmentStrategy_.onRemovedListener(removedListener); | |
298 | |
187 if (this.listeners_.length == 0) { | 299 if (this.listeners_.length == 0) { |
188 this.detach_(true); | 300 if (!this.eventName_) |
301 return; | |
302 | |
303 if (!attachedNamedEvents[this.eventName_]) { | |
304 throw new Error("chrome.Event '" + this.eventName_ + | |
305 "' is not attached."); | |
306 } | |
307 | |
308 delete attachedNamedEvents[this.eventName_]; | |
189 } | 309 } |
190 }; | 310 }; |
191 | 311 |
192 // Test if the given callback is registered for this event. | 312 // Test if the given callback is registered for this event. |
193 chrome.Event.prototype.hasListener = function(cb) { | 313 chrome.Event.prototype.hasListener = function(cb) { |
194 if (!this.eventOptions_.supportsListeners) | 314 if (!this.eventOptions_.supportsListeners) |
195 throw new Error("This event does not support listeners."); | 315 throw new Error("This event does not support listeners."); |
196 return this.findListener_(cb) > -1; | 316 return this.findListener_(cb) > -1; |
197 }; | 317 }; |
198 | 318 |
199 // Test if any callbacks are registered for this event. | 319 // Test if any callbacks are registered for this event. |
200 chrome.Event.prototype.hasListeners = function() { | 320 chrome.Event.prototype.hasListeners = function() { |
201 if (!this.eventOptions_.supportsListeners) | 321 if (!this.eventOptions_.supportsListeners) |
202 throw new Error("This event does not support listeners."); | 322 throw new Error("This event does not support listeners."); |
203 return this.listeners_.length > 0; | 323 return this.listeners_.length > 0; |
204 }; | 324 }; |
205 | 325 |
206 // Returns the index of the given callback if registered, or -1 if not | 326 // Returns the index of the given callback if registered, or -1 if not |
207 // found. | 327 // found. |
208 chrome.Event.prototype.findListener_ = function(cb) { | 328 chrome.Event.prototype.findListener_ = function(cb) { |
209 for (var i = 0; i < this.listeners_.length; i++) { | 329 for (var i = 0; i < this.listeners_.length; i++) { |
210 if (this.listeners_[i] == cb) { | 330 if (this.listeners_[i].callback == cb) { |
211 return i; | 331 return i; |
212 } | 332 } |
213 } | 333 } |
214 | 334 |
215 return -1; | 335 return -1; |
216 }; | 336 }; |
217 | 337 |
218 // Dispatches this event object to all listeners, passing all supplied | 338 chrome.Event.prototype.dispatch_ = function(args, listenerIDs) { |
219 // arguments to this function each listener. | |
220 chrome.Event.prototype.dispatch = function(varargs) { | |
221 if (!this.eventOptions_.supportsListeners) | 339 if (!this.eventOptions_.supportsListeners) |
222 throw new Error("This event does not support listeners."); | 340 throw new Error("This event does not support listeners."); |
223 var args = Array.prototype.slice.call(arguments); | |
224 var validationErrors = this.validateEventArgs_(args); | 341 var validationErrors = this.validateEventArgs_(args); |
225 if (validationErrors) { | 342 if (validationErrors) { |
226 return {validationErrors: validationErrors}; | 343 return {validationErrors: validationErrors}; |
227 } | 344 } |
345 | |
346 var listeners = this.attachmentStrategy_.getListenersByIDs(listenerIDs); | |
347 | |
228 var results = []; | 348 var results = []; |
229 for (var i = 0; i < this.listeners_.length; i++) { | 349 for (var i = 0; i < listeners.length; i++) { |
230 try { | 350 try { |
231 var result = this.listeners_[i].apply(null, args); | 351 var result = listeners[i].callback.apply(null, args); |
232 if (result !== undefined) | 352 if (result !== undefined) |
233 results.push(result); | 353 results.push(result); |
234 } catch (e) { | 354 } catch (e) { |
235 console.error("Error in event handler for '" + this.eventName_ + | 355 console.error("Error in event handler for '" + this.eventName_ + |
236 "': " + e.message + ' ' + e.stack); | 356 "': " + e.message + ' ' + e.stack); |
237 } | 357 } |
238 } | 358 } |
239 if (results.length) | 359 if (results.length) |
240 return {results: results}; | 360 return {results: results}; |
241 }; | 361 } |
242 | 362 |
243 // Attaches this event object to its name. Only one object can have a given | 363 // Dispatches this event object to all listeners, passing all supplied |
244 // name. | 364 // arguments to this function each listener. |
245 chrome.Event.prototype.attach_ = function() { | 365 chrome.Event.prototype.dispatch = function(varargs) { |
246 AttachEvent(this.eventName_); | 366 return this.dispatch_(Array.prototype.slice.call(arguments), undefined); |
247 allAttachedEvents[allAttachedEvents.length] = this; | |
248 if (!this.eventName_) | |
249 return; | |
250 | |
251 if (attachedNamedEvents[this.eventName_]) { | |
252 throw new Error("chrome.Event '" + this.eventName_ + | |
253 "' is already attached."); | |
254 } | |
255 | |
256 attachedNamedEvents[this.eventName_] = this; | |
257 }; | 367 }; |
258 | 368 |
259 // Detaches this event object from its name. | 369 // Detaches this event object from its name. |
260 chrome.Event.prototype.detach_ = function(manual) { | 370 chrome.Event.prototype.detach_ = function() { |
261 var i = allAttachedEvents.indexOf(this); | 371 this.attachmentStrategy_.detach(false); |
262 if (i >= 0) | |
263 delete allAttachedEvents[i]; | |
264 DetachEvent(this.eventName_, manual); | |
265 if (!this.eventName_) | |
266 return; | |
267 | |
268 if (!attachedNamedEvents[this.eventName_]) { | |
269 throw new Error("chrome.Event '" + this.eventName_ + | |
270 "' is not attached."); | |
271 } | |
272 | |
273 delete attachedNamedEvents[this.eventName_]; | |
274 }; | 372 }; |
275 | 373 |
276 chrome.Event.prototype.destroy_ = function() { | 374 chrome.Event.prototype.destroy_ = function() { |
277 this.listeners_ = []; | 375 this.listeners_ = []; |
278 this.validateEventArgs_ = []; | 376 this.validateEventArgs_ = []; |
279 this.detach_(false); | 377 this.detach_(false); |
280 }; | 378 }; |
281 | 379 |
380 // Gets the declarative API object, or undefined if this extension doesn't | |
381 // have access to it. | |
382 // | |
383 // This is defined as a function (rather than a variable) because it isn't | |
384 // accessible until the schema bindings have been generated. | |
385 function getDeclarativeAPI() { | |
Matt Perry
2012/06/13 01:24:27
is this used?
koz (OOO until 15th September)
2012/06/14 02:15:55
Hm, not sure where this came from. Removed.
| |
386 return chromeHidden.internalAPIs.declarative; | |
387 } | |
388 | |
282 chrome.Event.prototype.addRules = function(rules, opt_cb) { | 389 chrome.Event.prototype.addRules = function(rules, opt_cb) { |
283 if (!this.eventOptions_.supportsRules) | 390 if (!this.eventOptions_.supportsRules) |
284 throw new Error("This event does not support rules."); | 391 throw new Error("This event does not support rules."); |
285 | 392 |
286 // Takes a list of JSON datatype identifiers and returns a schema fragment | 393 // Takes a list of JSON datatype identifiers and returns a schema fragment |
287 // that verifies that a JSON object corresponds to an array of only these | 394 // that verifies that a JSON object corresponds to an array of only these |
288 // data types. | 395 // data types. |
289 function buildArrayOfChoicesSchema(typesList) { | 396 function buildArrayOfChoicesSchema(typesList) { |
290 return { | 397 return { |
291 'type': 'array', | 398 'type': 'array', |
(...skipping 58 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
350 chromeHidden.onUnload = new chrome.Event(); | 457 chromeHidden.onUnload = new chrome.Event(); |
351 | 458 |
352 chromeHidden.dispatchOnLoad = | 459 chromeHidden.dispatchOnLoad = |
353 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); | 460 chromeHidden.onLoad.dispatch.bind(chromeHidden.onLoad); |
354 | 461 |
355 chromeHidden.dispatchOnUnload = function() { | 462 chromeHidden.dispatchOnUnload = function() { |
356 chromeHidden.onUnload.dispatch(); | 463 chromeHidden.onUnload.dispatch(); |
357 for (var i = 0; i < allAttachedEvents.length; ++i) { | 464 for (var i = 0; i < allAttachedEvents.length; ++i) { |
358 var event = allAttachedEvents[i]; | 465 var event = allAttachedEvents[i]; |
359 if (event) | 466 if (event) |
360 event.detach_(false); | 467 event.detach_(); |
361 } | 468 } |
362 }; | 469 }; |
363 | 470 |
364 chromeHidden.dispatchError = function(msg) { | 471 chromeHidden.dispatchError = function(msg) { |
365 console.error(msg); | 472 console.error(msg); |
366 }; | 473 }; |
367 | 474 |
368 exports.Event = chrome.Event; | 475 exports.Event = chrome.Event; |
OLD | NEW |