Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(175)

Side by Side Diff: chrome/renderer/resources/extensions/event.js

Issue 10514013: Filtered events. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: split out event_filter changes Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
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
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
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
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;
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698