##// END OF EJS Templates
Merge pull request #7757 from jasongrout/custom-serialization...
Jonathan Frederic -
r21035:d6e249b0 merge
parent child Browse files
Show More
@@ -1,34 +1,34 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/manager",
6 "widgets/js/widget",
6 7 "widgets/js/widget_link",
7 8 "widgets/js/widget_bool",
8 9 "widgets/js/widget_button",
9 10 "widgets/js/widget_box",
10 11 "widgets/js/widget_float",
11 12 "widgets/js/widget_image",
12 13 "widgets/js/widget_int",
13 14 "widgets/js/widget_output",
14 15 "widgets/js/widget_selection",
15 16 "widgets/js/widget_selectioncontainer",
16 17 "widgets/js/widget_string",
17 ], function(widgetmanager, linkModels) {
18 for (var target_name in linkModels) {
19 if (linkModels.hasOwnProperty(target_name)) {
20 widgetmanager.WidgetManager.register_widget_model(target_name, linkModels[target_name]);
21 }
22 }
23
24 // Register all of the loaded views with the widget manager.
18 ], function(widgetmanager, widget) {
19 // Register all of the loaded models and views with the widget manager.
25 20 for (var i = 2; i < arguments.length; i++) {
26 for (var target_name in arguments[i]) {
27 if (arguments[i].hasOwnProperty(target_name)) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]);
21 var module = arguments[i];
22 for (var target_name in module) {
23 if (module.hasOwnProperty(target_name)) {
24 var target = module[target_name];
25 if (target.prototype instanceof widget.WidgetModel) {
26 widgetmanager.WidgetManager.register_widget_model(target_name, target);
27 } else if (target.prototype instanceof widget.WidgetView) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, target);
29 }
29 30 }
30 31 }
31 32 }
32
33 33 return {'WidgetManager': widgetmanager.WidgetManager};
34 34 });
@@ -1,763 +1,780 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 11 "use strict";
12 12
13 13 var WidgetModel = Backbone.Model.extend({
14 14 constructor: function (widget_manager, model_id, comm) {
15 15 /**
16 16 * Constructor
17 17 *
18 18 * Creates a WidgetModel instance.
19 19 *
20 20 * Parameters
21 21 * ----------
22 22 * widget_manager : WidgetManager instance
23 23 * model_id : string
24 24 * An ID unique to this model.
25 25 * comm : Comm instance (optional)
26 26 */
27 27 this.widget_manager = widget_manager;
28 28 this.state_change = Promise.resolve();
29 29 this._buffered_state_diff = {};
30 30 this.pending_msgs = 0;
31 31 this.msg_buffer = null;
32 32 this.state_lock = null;
33 33 this.id = model_id;
34 34 this.views = {};
35 35 this._resolve_received_state = {};
36 36
37 37 if (comm !== undefined) {
38 38 // Remember comm associated with the model.
39 39 this.comm = comm;
40 40 comm.model = this;
41 41
42 42 // Hook comm messages up to model.
43 43 comm.on_close($.proxy(this._handle_comm_closed, this));
44 44 comm.on_msg($.proxy(this._handle_comm_msg, this));
45 45
46 46 // Assume the comm is alive.
47 47 this.set_comm_live(true);
48 48 } else {
49 49 this.set_comm_live(false);
50 50 }
51 51
52 52 // Listen for the events that lead to the websocket being terminated.
53 53 var that = this;
54 54 var died = function() {
55 55 that.set_comm_live(false);
56 56 };
57 57 widget_manager.notebook.events.on('kernel_disconnected.Kernel', died);
58 58 widget_manager.notebook.events.on('kernel_killed.Kernel', died);
59 59 widget_manager.notebook.events.on('kernel_restarting.Kernel', died);
60 60 widget_manager.notebook.events.on('kernel_dead.Kernel', died);
61 61
62 62 return Backbone.Model.apply(this);
63 63 },
64 64
65 send: function (content, callbacks) {
65 send: function (content, callbacks, buffers) {
66 66 /**
67 67 * Send a custom msg over the comm.
68 68 */
69 69 if (this.comm !== undefined) {
70 70 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
71 this.comm.send(data, callbacks, {}, buffers);
72 72 this.pending_msgs++;
73 73 }
74 74 },
75 75
76 76 request_state: function(callbacks) {
77 77 /**
78 78 * Request a state push from the back-end.
79 79 */
80 80 if (!this.comm) {
81 81 console.error("Could not request_state because comm doesn't exist!");
82 82 return;
83 83 }
84 84
85 85 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
86 86
87 87 // Promise that is resolved when a state is received
88 88 // from the back-end.
89 89 var that = this;
90 90 var received_state = new Promise(function(resolve) {
91 91 that._resolve_received_state[msg_id] = resolve;
92 92 });
93 93 return received_state;
94 94 },
95 95
96 96 set_comm_live: function(live) {
97 97 /**
98 98 * Change the comm_live state of the model.
99 99 */
100 100 if (this.comm_live === undefined || this.comm_live != live) {
101 101 this.comm_live = live;
102 102 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
103 103 }
104 104 },
105 105
106 106 close: function(comm_closed) {
107 107 /**
108 108 * Close model
109 109 */
110 110 if (this.comm && !comm_closed) {
111 111 this.comm.close();
112 112 }
113 113 this.stopListening();
114 114 this.trigger('destroy', this);
115 115 delete this.comm.model; // Delete ref so GC will collect widget model.
116 116 delete this.comm;
117 117 delete this.model_id; // Delete id from model so widget manager cleans up.
118 118 _.each(this.views, function(v, id, views) {
119 119 v.then(function(view) {
120 120 view.remove();
121 121 delete views[id];
122 122 });
123 123 });
124 124 },
125 125
126 126 _handle_comm_closed: function (msg) {
127 127 /**
128 128 * Handle when a widget is closed.
129 129 */
130 130 this.trigger('comm:close');
131 131 this.close(true);
132 132 },
133 133
134 134 _handle_comm_msg: function (msg) {
135 135 /**
136 136 * Handle incoming comm msg.
137 137 */
138 138 var method = msg.content.data.method;
139
139 140 var that = this;
140 141 switch (method) {
141 142 case 'update':
142 143 this.state_change = this.state_change
143 144 .then(function() {
144 return that.set_state(msg.content.data.state);
145 var state = msg.content.data.state || {};
146 var buffer_keys = msg.content.data.buffers || [];
147 var buffers = msg.buffers || [];
148 for (var i=0; i<buffer_keys.length; i++) {
149 state[buffer_keys[i]] = buffers[i];
150 }
151
152 // deserialize fields that have custom deserializers
153 var serializers = that.constructor.serializers;
154 if (serializers) {
155 for (var k in state) {
156 if (serializers[k] && serializers[k].deserialize) {
157 state[k] = (serializers[k].deserialize)(state[k], that);
158 }
159 }
160 }
161 return utils.resolve_promises_dict(state);
162 }).then(function(state) {
163 return that.set_state(state);
145 164 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 165 .then(function() {
147 166 var parent_id = msg.parent_header.msg_id;
148 167 if (that._resolve_received_state[parent_id] !== undefined) {
149 168 that._resolve_received_state[parent_id].call();
150 169 delete that._resolve_received_state[parent_id];
151 170 }
152 171 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 172 break;
154 173 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
174 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 175 break;
157 176 case 'display':
158 177 this.state_change = this.state_change.then(function() {
159 178 that.widget_manager.display_view(msg, that);
160 179 }).catch(utils.reject('Could not process display view msg', true));
161 180 break;
162 181 }
163 182 },
164 183
165 184 set_state: function (state) {
166 185 var that = this;
167 186 // Handle when a widget is updated via the python side.
168 return this._unpack_models(state).then(function(state) {
187 return new Promise(function(resolve, reject) {
169 188 that.state_lock = state;
170 189 try {
171 190 WidgetModel.__super__.set.call(that, state);
172 191 } finally {
173 192 that.state_lock = null;
174 193 }
194 resolve();
175 195 }).catch(utils.reject("Couldn't set model state", true));
176 196 },
177 197
178 198 get_state: function() {
179 199 // Get the serializable state of the model.
180 var state = this.toJSON();
181 for (var key in state) {
182 if (state.hasOwnProperty(key)) {
183 state[key] = this._pack_models(state[key]);
184 }
185 }
186 return state;
200 // Equivalent to Backbone.Model.toJSON()
201 return _.clone(this.attributes);
187 202 },
188
203
189 204 _handle_status: function (msg, callbacks) {
190 205 /**
191 206 * Handle status msgs.
192 207 *
193 208 * execution_state : ('busy', 'idle', 'starting')
194 209 */
195 210 if (this.comm !== undefined) {
196 211 if (msg.content.execution_state ==='idle') {
197 212 // Send buffer if this message caused another message to be
198 213 // throttled.
199 214 if (this.msg_buffer !== null &&
200 215 (this.get('msg_throttle') || 3) === this.pending_msgs) {
201 216 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
202 217 this.comm.send(data, callbacks);
203 218 this.msg_buffer = null;
204 219 } else {
205 220 --this.pending_msgs;
206 221 }
207 222 }
208 223 }
209 224 },
210 225
211 226 callbacks: function(view) {
212 227 /**
213 228 * Create msg callbacks for a comm msg.
214 229 */
215 230 var callbacks = this.widget_manager.callbacks(view);
216 231
217 232 if (callbacks.iopub === undefined) {
218 233 callbacks.iopub = {};
219 234 }
220 235
221 236 var that = this;
222 237 callbacks.iopub.status = function (msg) {
223 238 that._handle_status(msg, callbacks);
224 239 };
225 240 return callbacks;
226 241 },
227 242
228 243 set: function(key, val, options) {
229 244 /**
230 245 * Set a value.
231 246 */
232 247 var return_value = WidgetModel.__super__.set.apply(this, arguments);
233 248
234 249 // Backbone only remembers the diff of the most recent set()
235 250 // operation. Calling set multiple times in a row results in a
236 251 // loss of diff information. Here we keep our own running diff.
237 252 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
238 253 return return_value;
239 254 },
240 255
241 256 sync: function (method, model, options) {
242 257 /**
243 258 * Handle sync to the back-end. Called when a model.save() is called.
244 259 *
245 260 * Make sure a comm exists.
261
262 * Parameters
263 * ----------
264 * method : create, update, patch, delete, read
265 * create/update always send the full attribute set
266 * patch - only send attributes listed in options.attrs, and if we are queuing
267 * up messages, combine with previous messages that have not been sent yet
268 * model : the model we are syncing
269 * will normally be the same as `this`
270 * options : dict
271 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
272 * otherwise, sync all attributes
273 *
246 274 */
247 275 var error = options.error || function() {
248 276 console.error('Backbone sync error:', arguments);
249 277 };
250 278 if (this.comm === undefined) {
251 279 error();
252 280 return false;
253 281 }
254 282
255 // Delete any key value pairs that the back-end already knows about.
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
283 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
284
285 // the state_lock lists attributes that are currently be changed right now from a kernel message
286 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
287 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
257 288 if (this.state_lock !== null) {
258 289 var keys = Object.keys(this.state_lock);
259 290 for (var i=0; i<keys.length; i++) {
260 291 var key = keys[i];
261 292 if (attrs[key] === this.state_lock[key]) {
262 293 delete attrs[key];
263 294 }
264 295 }
265 296 }
266
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
297
269 298 if (_.size(attrs) > 0) {
270 299
271 300 // If this message was sent via backbone itself, it will not
272 301 // have any callbacks. It's important that we create callbacks
273 302 // so we can listen for status messages, etc...
274 303 var callbacks = options.callbacks || this.callbacks();
275 304
276 305 // Check throttle.
277 306 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
278 307 // The throttle has been exceeded, buffer the current msg so
279 308 // it can be sent once the kernel has finished processing
280 309 // some of the existing messages.
281 310
282 311 // Combine updates if it is a 'patch' sync, otherwise replace updates
283 312 switch (method) {
284 313 case 'patch':
285 314 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
286 315 break;
287 316 case 'update':
288 317 case 'create':
289 318 this.msg_buffer = attrs;
290 319 break;
291 320 default:
292 321 error();
293 322 return false;
294 323 }
295 324 this.msg_buffer_callbacks = callbacks;
296 325
297 326 } else {
298 327 // We haven't exceeded the throttle, send the message like
299 328 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
301 this.comm.send(data, callbacks);
329 this.send_sync_message(attrs, callbacks);
302 330 this.pending_msgs++;
303 331 }
304 332 }
305 333 // Since the comm is a one-way communication, assume the message
306 334 // arrived. Don't call success since we don't have a model back from the server
307 335 // this means we miss out on the 'sync' event.
308 336 this._buffered_state_diff = {};
309 337 },
310 338
339
340 send_sync_message: function(attrs, callbacks) {
341 // prepare and send a comm message syncing attrs
342 var that = this;
343 // first, build a state dictionary with key=the attribute and the value
344 // being the value or the promise of the serialized value
345 var serializers = this.constructor.serializers;
346 if (serializers) {
347 for (k in attrs) {
348 if (serializers[k] && serializers[k].serialize) {
349 attrs[k] = (serializers[k].serialize)(attrs[k], this);
350 }
351 }
352 }
353 utils.resolve_promises_dict(attrs).then(function(state) {
354 // get binary values, then send
355 var keys = Object.keys(state);
356 var buffers = [];
357 var buffer_keys = [];
358 for (var i=0; i<keys.length; i++) {
359 var key = keys[i];
360 var value = state[key];
361 if (value.buffer instanceof ArrayBuffer
362 || value instanceof ArrayBuffer) {
363 buffers.push(value);
364 buffer_keys.push(key);
365 delete state[key];
366 }
367 }
368 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
369 }).catch(function(error) {
370 that.pending_msgs--;
371 return (utils.reject("Couldn't send widget sync message", true))(error);
372 });
373 },
374
311 375 save_changes: function(callbacks) {
312 376 /**
313 377 * Push this model's state to the back-end
314 378 *
315 379 * This invokes a Backbone.Sync.
316 380 */
317 381 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
318 382 },
319 383
320 _pack_models: function(value) {
321 /**
322 * Replace models with model ids recursively.
323 */
324 var that = this;
325 var packed;
326 if (value instanceof Backbone.Model) {
327 return "IPY_MODEL_" + value.id;
328
329 } else if ($.isArray(value)) {
330 packed = [];
331 _.each(value, function(sub_value, key) {
332 packed.push(that._pack_models(sub_value));
333 });
334 return packed;
335 } else if (value instanceof Date || value instanceof String) {
336 return value;
337 } else if (value instanceof Object) {
338 packed = {};
339 _.each(value, function(sub_value, key) {
340 packed[key] = that._pack_models(sub_value);
341 });
342 return packed;
343
344 } else {
345 return value;
346 }
347 },
348
349 _unpack_models: function(value) {
350 /**
351 * Replace model ids with models recursively.
352 */
353 var that = this;
354 var unpacked;
355 if ($.isArray(value)) {
356 unpacked = [];
357 _.each(value, function(sub_value, key) {
358 unpacked.push(that._unpack_models(sub_value));
359 });
360 return Promise.all(unpacked);
361 } else if (value instanceof Object) {
362 unpacked = {};
363 _.each(value, function(sub_value, key) {
364 unpacked[key] = that._unpack_models(sub_value);
365 });
366 return utils.resolve_promises_dict(unpacked);
367 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
368 // get_model returns a promise already
369 return this.widget_manager.get_model(value.slice(10, value.length));
370 } else {
371 return Promise.resolve(value);
372 }
373 },
374
375 384 on_some_change: function(keys, callback, context) {
376 385 /**
377 386 * on_some_change(["key1", "key2"], foo, context) differs from
378 387 * on("change:key1 change:key2", foo, context).
379 388 * If the widget attributes key1 and key2 are both modified,
380 389 * the second form will result in foo being called twice
381 390 * while the first will call foo only once.
382 391 */
383 392 this.on('change', function() {
384 393 if (keys.some(this.hasChanged, this)) {
385 394 callback.apply(context);
386 395 }
387 396 }, this);
388 397
389 },
398 },
399
400 toJSON: function(options) {
401 /**
402 * Serialize the model. See the types.js deserialization function
403 * and the kernel-side serializer/deserializer
404 */
405 return "IPY_MODEL_"+this.id;
406 }
390 407 });
391 408 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
392 409
393 410
394 411 var WidgetView = Backbone.View.extend({
395 412 initialize: function(parameters) {
396 413 /**
397 414 * Public constructor.
398 415 */
399 416 this.model.on('change',this.update,this);
400 417
401 418 // Bubble the comm live events.
402 419 this.model.on('comm:live', function() {
403 420 this.trigger('comm:live', this);
404 421 }, this);
405 422 this.model.on('comm:dead', function() {
406 423 this.trigger('comm:dead', this);
407 424 }, this);
408 425
409 426 this.options = parameters.options;
410 427 this.on('displayed', function() {
411 428 this.is_displayed = true;
412 429 }, this);
413 430 },
414 431
415 432 update: function(){
416 433 /**
417 434 * Triggered on model change.
418 435 *
419 436 * Update view to be consistent with this.model
420 437 */
421 438 },
422 439
423 440 create_child_view: function(child_model, options) {
424 441 /**
425 442 * Create and promise that resolves to a child view of a given model
426 443 */
427 444 var that = this;
428 445 options = $.extend({ parent: this }, options || {});
429 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
446 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true));
430 447 },
431 448
432 449 callbacks: function(){
433 450 /**
434 451 * Create msg callbacks for a comm msg.
435 452 */
436 453 return this.model.callbacks(this);
437 454 },
438 455
439 456 render: function(){
440 457 /**
441 458 * Render the view.
442 459 *
443 460 * By default, this is only called the first time the view is created
444 461 */
445 462 },
446 463
447 send: function (content) {
464 send: function (content, buffers) {
448 465 /**
449 466 * Send a custom msg associated with this view.
450 467 */
451 this.model.send(content, this.callbacks());
468 this.model.send(content, this.callbacks(), buffers);
452 469 },
453 470
454 471 touch: function () {
455 472 this.model.save_changes(this.callbacks());
456 473 },
457 474
458 475 after_displayed: function (callback, context) {
459 476 /**
460 477 * Calls the callback right away is the view is already displayed
461 478 * otherwise, register the callback to the 'displayed' event.
462 479 */
463 480 if (this.is_displayed) {
464 481 callback.apply(context);
465 482 } else {
466 483 this.on('displayed', callback, context);
467 484 }
468 485 },
469 486
470 487 remove: function () {
471 488 // Raise a remove event when the view is removed.
472 489 WidgetView.__super__.remove.apply(this, arguments);
473 490 this.trigger('remove');
474 491 }
475 492 });
476 493
477 494
478 495 var DOMWidgetView = WidgetView.extend({
479 496 initialize: function (parameters) {
480 497 /**
481 498 * Public constructor
482 499 */
483 500 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
484 501 this.model.on('change:visible', this.update_visible, this);
485 502 this.model.on('change:_css', this.update_css, this);
486 503
487 504 this.model.on('change:_dom_classes', function(model, new_classes) {
488 505 var old_classes = model.previous('_dom_classes');
489 506 this.update_classes(old_classes, new_classes);
490 507 }, this);
491 508
492 509 this.model.on('change:color', function (model, value) {
493 510 this.update_attr('color', value); }, this);
494 511
495 512 this.model.on('change:background_color', function (model, value) {
496 513 this.update_attr('background', value); }, this);
497 514
498 515 this.model.on('change:width', function (model, value) {
499 516 this.update_attr('width', value); }, this);
500 517
501 518 this.model.on('change:height', function (model, value) {
502 519 this.update_attr('height', value); }, this);
503 520
504 521 this.model.on('change:border_color', function (model, value) {
505 522 this.update_attr('border-color', value); }, this);
506 523
507 524 this.model.on('change:border_width', function (model, value) {
508 525 this.update_attr('border-width', value); }, this);
509 526
510 527 this.model.on('change:border_style', function (model, value) {
511 528 this.update_attr('border-style', value); }, this);
512 529
513 530 this.model.on('change:font_style', function (model, value) {
514 531 this.update_attr('font-style', value); }, this);
515 532
516 533 this.model.on('change:font_weight', function (model, value) {
517 534 this.update_attr('font-weight', value); }, this);
518 535
519 536 this.model.on('change:font_size', function (model, value) {
520 537 this.update_attr('font-size', this._default_px(value)); }, this);
521 538
522 539 this.model.on('change:font_family', function (model, value) {
523 540 this.update_attr('font-family', value); }, this);
524 541
525 542 this.model.on('change:padding', function (model, value) {
526 543 this.update_attr('padding', value); }, this);
527 544
528 545 this.model.on('change:margin', function (model, value) {
529 546 this.update_attr('margin', this._default_px(value)); }, this);
530 547
531 548 this.model.on('change:border_radius', function (model, value) {
532 549 this.update_attr('border-radius', this._default_px(value)); }, this);
533 550
534 551 this.after_displayed(function() {
535 552 this.update_visible(this.model, this.model.get("visible"));
536 553 this.update_classes([], this.model.get('_dom_classes'));
537 554
538 555 this.update_attr('color', this.model.get('color'));
539 556 this.update_attr('background', this.model.get('background_color'));
540 557 this.update_attr('width', this.model.get('width'));
541 558 this.update_attr('height', this.model.get('height'));
542 559 this.update_attr('border-color', this.model.get('border_color'));
543 560 this.update_attr('border-width', this.model.get('border_width'));
544 561 this.update_attr('border-style', this.model.get('border_style'));
545 562 this.update_attr('font-style', this.model.get('font_style'));
546 563 this.update_attr('font-weight', this.model.get('font_weight'));
547 564 this.update_attr('font-size', this._default_px(this.model.get('font_size')));
548 565 this.update_attr('font-family', this.model.get('font_family'));
549 566 this.update_attr('padding', this.model.get('padding'));
550 567 this.update_attr('margin', this._default_px(this.model.get('margin')));
551 568 this.update_attr('border-radius', this._default_px(this.model.get('border_radius')));
552 569
553 570 this.update_css(this.model, this.model.get("_css"));
554 571 }, this);
555 572 },
556 573
557 574 _default_px: function(value) {
558 575 /**
559 576 * Makes browser interpret a numerical string as a pixel value.
560 577 */
561 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
578 if (value && /^\d+\.?(\d+)?$/.test(value.trim())) {
562 579 return value.trim() + 'px';
563 580 }
564 581 return value;
565 582 },
566 583
567 584 update_attr: function(name, value) {
568 585 /**
569 586 * Set a css attr of the widget view.
570 587 */
571 588 this.$el.css(name, value);
572 589 },
573 590
574 591 update_visible: function(model, value) {
575 592 /**
576 593 * Update visibility
577 594 */
578 595 switch(value) {
579 596 case null: // python None
580 597 this.$el.show().css('visibility', 'hidden'); break;
581 598 case false:
582 599 this.$el.hide(); break;
583 600 case true:
584 601 this.$el.show().css('visibility', ''); break;
585 602 }
586 603 },
587 604
588 605 update_css: function (model, css) {
589 606 /**
590 607 * Update the css styling of this view.
591 608 */
592 609 if (css === undefined) {return;}
593 610 for (var i = 0; i < css.length; i++) {
594 611 // Apply the css traits to all elements that match the selector.
595 612 var selector = css[i][0];
596 613 var elements = this._get_selector_element(selector);
597 614 if (elements.length > 0) {
598 615 var trait_key = css[i][1];
599 616 var trait_value = css[i][2];
600 617 elements.css(trait_key ,trait_value);
601 618 }
602 619 }
603 620 },
604 621
605 622 update_classes: function (old_classes, new_classes, $el) {
606 623 /**
607 624 * Update the DOM classes applied to an element, default to this.$el.
608 625 */
609 626 if ($el===undefined) {
610 627 $el = this.$el;
611 628 }
612 629 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
613 630 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
614 631 },
615 632
616 633 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
617 634 /**
618 635 * Update the DOM classes applied to the widget based on a single
619 636 * trait's value.
620 637 *
621 638 * Given a trait value classes map, this function automatically
622 639 * handles applying the appropriate classes to the widget element
623 640 * and removing classes that are no longer valid.
624 641 *
625 642 * Parameters
626 643 * ----------
627 644 * class_map: dictionary
628 645 * Dictionary of trait values to class lists.
629 646 * Example:
630 647 * {
631 648 * success: ['alert', 'alert-success'],
632 649 * info: ['alert', 'alert-info'],
633 650 * warning: ['alert', 'alert-warning'],
634 651 * danger: ['alert', 'alert-danger']
635 652 * };
636 653 * trait_name: string
637 654 * Name of the trait to check the value of.
638 655 * previous_trait_value: optional string, default ''
639 656 * Last trait value
640 657 * $el: optional jQuery element handle, defaults to this.$el
641 658 * Element that the classes are applied to.
642 659 */
643 660 var key = previous_trait_value;
644 661 if (key === undefined) {
645 662 key = this.model.previous(trait_name);
646 663 }
647 664 var old_classes = class_map[key] ? class_map[key] : [];
648 665 key = this.model.get(trait_name);
649 666 var new_classes = class_map[key] ? class_map[key] : [];
650 667
651 668 this.update_classes(old_classes, new_classes, $el || this.$el);
652 669 },
653 670
654 671 _get_selector_element: function (selector) {
655 672 /**
656 673 * Get the elements via the css selector.
657 674 */
658 675 var elements;
659 676 if (!selector) {
660 677 elements = this.$el;
661 678 } else {
662 679 elements = this.$el.find(selector).addBack(selector);
663 680 }
664 681 return elements;
665 682 },
666 683
667 684 typeset: function(element, text){
668 685 utils.typeset.apply(null, arguments);
669 686 },
670 687 });
671 688
672 689
673 690 var ViewList = function(create_view, remove_view, context) {
674 691 /**
675 692 * - create_view and remove_view are default functions called when adding or removing views
676 693 * - create_view takes a model and returns a view or a promise for a view for that model
677 694 * - remove_view takes a view and destroys it (including calling `view.remove()`)
678 695 * - each time the update() function is called with a new list, the create and remove
679 696 * callbacks will be called in an order so that if you append the views created in the
680 697 * create callback and remove the views in the remove callback, you will duplicate
681 698 * the order of the list.
682 699 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
683 700 * - the context defaults to the created ViewList. If you pass another context, the create and remove
684 701 * will be called in that context.
685 702 */
686 703
687 704 this.initialize.apply(this, arguments);
688 705 };
689 706
690 707 _.extend(ViewList.prototype, {
691 708 initialize: function(create_view, remove_view, context) {
692 709 this._handler_context = context || this;
693 710 this._models = [];
694 711 this.views = []; // list of promises for views
695 712 this._create_view = create_view;
696 713 this._remove_view = remove_view || function(view) {view.remove();};
697 714 },
698 715
699 716 update: function(new_models, create_view, remove_view, context) {
700 717 /**
701 718 * the create_view, remove_view, and context arguments override the defaults
702 719 * specified when the list is created.
703 720 * after this function, the .views attribute is a list of promises for views
704 721 * if you want to perform some action on the list of views, do something like
705 722 * `Promise.all(myviewlist.views).then(function(views) {...});`
706 723 */
707 724 var remove = remove_view || this._remove_view;
708 725 var create = create_view || this._create_view;
709 726 context = context || this._handler_context;
710 727 var i = 0;
711 728 // first, skip past the beginning of the lists if they are identical
712 729 for (; i < new_models.length; i++) {
713 730 if (i >= this._models.length || new_models[i] !== this._models[i]) {
714 731 break;
715 732 }
716 733 }
717 734
718 735 var first_removed = i;
719 736 // Remove the non-matching items from the old list.
720 737 var removed = this.views.splice(first_removed, this.views.length-first_removed);
721 738 for (var j = 0; j < removed.length; j++) {
722 739 removed[j].then(function(view) {
723 740 remove.call(context, view)
724 741 });
725 742 }
726 743
727 744 // Add the rest of the new list items.
728 745 for (; i < new_models.length; i++) {
729 746 this.views.push(Promise.resolve(create.call(context, new_models[i])));
730 747 }
731 748 // make a copy of the input array
732 749 this._models = new_models.slice();
733 750 },
734 751
735 752 remove: function() {
736 753 /**
737 754 * removes every view in the list; convenience function for `.update([])`
738 755 * that should be faster
739 756 * returns a promise that resolves after this removal is done
740 757 */
741 758 var that = this;
742 759 return Promise.all(this.views).then(function(views) {
743 760 for (var i = 0; i < that.views.length; i++) {
744 761 that._remove_view.call(that._handler_context, views[i]);
745 762 }
746 763 that.views = [];
747 764 that._models = [];
748 765 });
749 766 },
750 767 });
751 768
752 769 var widget = {
753 770 'WidgetModel': WidgetModel,
754 771 'WidgetView': WidgetView,
755 772 'DOMWidgetView': DOMWidgetView,
756 773 'ViewList': ViewList,
757 774 };
758 775
759 776 // For backwards compatability.
760 777 $.extend(IPython, widget);
761 778
762 779 return widget;
763 780 });
@@ -1,154 +1,187 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 6 "jqueryui",
7 "underscore",
7 8 "base/js/utils",
8 9 "bootstrap",
9 ], function(widget, $, utils){
10 ], function(widget, $, _, utils){
10 11 "use strict";
12 var unpack_models = function unpack_models(value, model) {
13 /**
14 * Replace model ids with models recursively.
15 */
16 var unpacked;
17 if ($.isArray(value)) {
18 unpacked = [];
19 _.each(value, function(sub_value, key) {
20 unpacked.push(unpack_models(sub_value, model));
21 });
22 return Promise.all(unpacked);
23 } else if (value instanceof Object) {
24 unpacked = {};
25 _.each(value, function(sub_value, key) {
26 unpacked[key] = unpack_models(sub_value, model);
27 });
28 return utils.resolve_promises_dict(unpacked);
29 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
30 // get_model returns a promise already
31 return model.widget_manager.get_model(value.slice(10, value.length));
32 } else {
33 return Promise.resolve(value);
34 }
35 };
36
37 var BoxModel = widget.WidgetModel.extend({}, {
38 serializers: _.extend({
39 children: {deserialize: unpack_models}
40 }, widget.WidgetModel.serializers)
41 });
11 42
12 43 var BoxView = widget.DOMWidgetView.extend({
13 44 initialize: function(){
14 45 /**
15 46 * Public constructor
16 47 */
17 48 BoxView.__super__.initialize.apply(this, arguments);
18 49 this.children_views = new widget.ViewList(this.add_child_model, null, this);
19 50 this.listenTo(this.model, 'change:children', function(model, value) {
20 51 this.children_views.update(value);
21 52 }, this);
22 53 this.listenTo(this.model, 'change:overflow_x', function(model, value) {
23 54 this.update_overflow_x();
24 55 }, this);
25 56 this.listenTo(this.model, 'change:overflow_y', function(model, value) {
26 57 this.update_overflow_y();
27 58 }, this);
28 59 this.listenTo(this.model, 'change:box_style', function(model, value) {
29 60 this.update_box_style();
30 61 }, this);
31 62 },
32 63
33 64 update_attr: function(name, value) {
34 65 /**
35 66 * Set a css attr of the widget view.
36 67 */
37 68 this.$box.css(name, value);
38 69 },
39 70
40 71 render: function(){
41 72 /**
42 73 * Called when view is rendered.
43 74 */
44 75 this.$box = this.$el;
45 76 this.$box.addClass('widget-box');
46 77 this.children_views.update(this.model.get('children'));
47 78 this.update_overflow_x();
48 79 this.update_overflow_y();
49 80 this.update_box_style('');
50 81 },
51 82
52 83 update_overflow_x: function() {
53 84 /**
54 85 * Called when the x-axis overflow setting is changed.
55 86 */
56 87 this.$box.css('overflow-x', this.model.get('overflow_x'));
57 88 },
58 89
59 90 update_overflow_y: function() {
60 91 /**
61 92 * Called when the y-axis overflow setting is changed.
62 93 */
63 94 this.$box.css('overflow-y', this.model.get('overflow_y'));
64 95 },
65 96
66 97 update_box_style: function(previous_trait_value) {
67 98 var class_map = {
68 99 success: ['alert', 'alert-success'],
69 100 info: ['alert', 'alert-info'],
70 101 warning: ['alert', 'alert-warning'],
71 102 danger: ['alert', 'alert-danger']
72 103 };
73 104 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
74 105 },
75 106
76 107 add_child_model: function(model) {
77 108 /**
78 109 * Called when a model is added to the children list.
79 110 */
80 111 var that = this;
81 112 var dummy = $('<div/>');
82 113 that.$box.append(dummy);
83 114 return this.create_child_view(model).then(function(view) {
84 115 dummy.replaceWith(view.el);
85 116
86 117 // Trigger the displayed event of the child view.
87 118 that.after_displayed(function() {
88 119 view.trigger('displayed');
89 120 });
90 121 return view;
91 122 }).catch(utils.reject("Couldn't add child view to box", true));
92 123 },
93 124
94 125 remove: function() {
95 126 /**
96 127 * We remove this widget before removing the children as an optimization
97 128 * we want to remove the entire container from the DOM first before
98 129 * removing each individual child separately.
99 130 */
100 131 BoxView.__super__.remove.apply(this, arguments);
101 132 this.children_views.remove();
102 133 },
103 134 });
104 135
105 136
106 137 var FlexBoxView = BoxView.extend({
107 138 render: function(){
108 139 FlexBoxView.__super__.render.apply(this);
109 140 this.listenTo(this.model, 'change:orientation', this.update_orientation, this);
110 141 this.listenTo(this.model, 'change:flex', this._flex_changed, this);
111 142 this.listenTo(this.model, 'change:pack', this._pack_changed, this);
112 143 this.listenTo(this.model, 'change:align', this._align_changed, this);
113 144 this._flex_changed();
114 145 this._pack_changed();
115 146 this._align_changed();
116 147 this.update_orientation();
117 148 },
118 149
119 150 update_orientation: function(){
120 151 var orientation = this.model.get("orientation");
121 152 if (orientation == "vertical") {
122 153 this.$box.removeClass("hbox").addClass("vbox");
123 154 } else {
124 155 this.$box.removeClass("vbox").addClass("hbox");
125 156 }
126 157 },
127 158
128 159 _flex_changed: function(){
129 160 if (this.model.previous('flex')) {
130 161 this.$box.removeClass('box-flex' + this.model.previous('flex'));
131 162 }
132 163 this.$box.addClass('box-flex' + this.model.get('flex'));
133 164 },
134 165
135 166 _pack_changed: function(){
136 167 if (this.model.previous('pack')) {
137 168 this.$box.removeClass(this.model.previous('pack'));
138 169 }
139 170 this.$box.addClass(this.model.get('pack'));
140 171 },
141 172
142 173 _align_changed: function(){
143 174 if (this.model.previous('align')) {
144 175 this.$box.removeClass('align-' + this.model.previous('align'));
145 176 }
146 177 this.$box.addClass('align-' + this.model.get('align'));
147 178 },
148 179 });
149 180
150 181 return {
182 'unpack_models': unpack_models,
183 'BoxModel': BoxModel,
151 184 'BoxView': BoxView,
152 185 'FlexBoxView': FlexBoxView,
153 186 };
154 187 });
@@ -1,832 +1,845 b''
1 1 //
2 2 // Utility functions for the HTML notebook's CasperJS tests.
3 3 //
4 4 casper.get_notebook_server = function () {
5 5 // Get the URL of a notebook server on which to run tests.
6 6 var port = casper.cli.get("port");
7 7 port = (typeof port === 'undefined') ? '8888' : port;
8 8 return casper.cli.get("url") || ('http://127.0.0.1:' + port);
9 9 };
10 10
11 11 casper.open_new_notebook = function () {
12 12 // Create and open a new notebook.
13 13 var baseUrl = this.get_notebook_server();
14 14 this.start(baseUrl);
15 15 this.waitFor(this.page_loaded);
16 16 this.waitForSelector('#kernel-python2 a, #kernel-python3 a');
17 17 this.thenClick('#kernel-python2 a, #kernel-python3 a');
18 18
19 19 this.waitForPopup('');
20 20
21 21 this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');});
22 22 this.then(function () {
23 23 this.open(this.popups[0].url);
24 24 });
25 25 this.waitFor(this.page_loaded);
26 26
27 27 // Hook the log and error methods of the console, forcing them to
28 28 // serialize their arguments before printing. This allows the
29 29 // Objects to cross into the phantom/slimer regime for display.
30 30 this.thenEvaluate(function(){
31 31 var serialize_arguments = function(f, context) {
32 32 return function() {
33 33 var pretty_arguments = [];
34 34 for (var i = 0; i < arguments.length; i++) {
35 35 var value = arguments[i];
36 36 if (value instanceof Object) {
37 37 var name = value.name || 'Object';
38 38 // Print a JSON string representation of the object.
39 39 // If we don't do this, [Object object] gets printed
40 40 // by casper, which is useless. The long regular
41 41 // expression reduces the verbosity of the JSON.
42 42 pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ')
43 43 .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n')
44 44 .replace(/\n(\s+)?\n/g, '\n'));
45 45 } else {
46 46 pretty_arguments.push(value);
47 47 }
48 48 }
49 49 f.apply(context, pretty_arguments);
50 50 };
51 51 };
52 52 console.log = serialize_arguments(console.log, console);
53 53 console.error = serialize_arguments(console.error, console);
54 54 });
55 55
56 56 // Make sure the kernel has started
57 57 this.waitFor(this.kernel_running);
58 58 // track the IPython busy/idle state
59 59 this.thenEvaluate(function () {
60 60 require(['base/js/namespace', 'base/js/events'], function (IPython, events) {
61 61
62 62 events.on('kernel_idle.Kernel',function () {
63 63 IPython._status = 'idle';
64 64 });
65 65 events.on('kernel_busy.Kernel',function () {
66 66 IPython._status = 'busy';
67 67 });
68 68 });
69 69 });
70 70
71 71 // Because of the asynchronous nature of SlimerJS (Gecko), we need to make
72 72 // sure the notebook has actually been loaded into the IPython namespace
73 73 // before running any tests.
74 74 this.waitFor(function() {
75 75 return this.evaluate(function () {
76 76 return IPython.notebook;
77 77 });
78 78 });
79 79 };
80 80
81 81 casper.page_loaded = function() {
82 82 // Return whether or not the kernel is running.
83 83 return this.evaluate(function() {
84 84 return typeof IPython !== "undefined" &&
85 85 IPython.page !== undefined;
86 86 });
87 87 };
88 88
89 89 casper.kernel_running = function() {
90 90 // Return whether or not the kernel is running.
91 91 return this.evaluate(function() {
92 92 return IPython &&
93 93 IPython.notebook &&
94 94 IPython.notebook.kernel &&
95 95 IPython.notebook.kernel.is_connected();
96 96 });
97 97 };
98 98
99 99 casper.kernel_disconnected = function() {
100 100 return this.evaluate(function() {
101 101 return IPython.notebook.kernel.is_fully_disconnected();
102 102 });
103 103 };
104 104
105 105 casper.wait_for_kernel_ready = function () {
106 106 this.waitFor(this.kernel_running);
107 107 this.thenEvaluate(function () {
108 108 IPython._kernel_ready = false;
109 109 IPython.notebook.kernel.kernel_info(
110 110 function () {
111 111 IPython._kernel_ready = true;
112 112 });
113 113 });
114 114 this.waitFor(function () {
115 115 return this.evaluate(function () {
116 116 return IPython._kernel_ready;
117 117 });
118 118 });
119 119 };
120 120
121 121 casper.shutdown_current_kernel = function () {
122 122 // Shut down the current notebook's kernel.
123 123 this.thenEvaluate(function() {
124 124 IPython.notebook.session.delete();
125 125 });
126 126 // We close the page right after this so we need to give it time to complete.
127 127 this.wait(1000);
128 128 };
129 129
130 130 casper.delete_current_notebook = function () {
131 131 // Delete created notebook.
132 132
133 133 // For some unknown reason, this doesn't work?!?
134 134 this.thenEvaluate(function() {
135 135 IPython.notebook.delete();
136 136 });
137 137 };
138 138
139 139 casper.wait_for_busy = function () {
140 140 // Waits for the notebook to enter a busy state.
141 141 this.waitFor(function () {
142 142 return this.evaluate(function () {
143 143 return IPython._status == 'busy';
144 144 });
145 145 });
146 146 };
147 147
148 148 casper.wait_for_idle = function () {
149 149 // Waits for the notebook to idle.
150 150 this.waitFor(function () {
151 151 return this.evaluate(function () {
152 152 return IPython._status == 'idle';
153 153 });
154 154 });
155 155 };
156 156
157 157 casper.wait_for_output = function (cell_num, out_num) {
158 158 // wait for the nth output in a given cell
159 159 this.wait_for_idle();
160 160 out_num = out_num || 0;
161 161 this.then(function() {
162 162 this.waitFor(function (c, o) {
163 163 return this.evaluate(function get_output(c, o) {
164 164 var cell = IPython.notebook.get_cell(c);
165 165 return cell.output_area.outputs.length > o;
166 166 },
167 167 // pass parameter from the test suite js to the browser code js
168 168 {c : cell_num, o : out_num});
169 169 });
170 170 },
171 171 function then() { },
172 172 function timeout() {
173 173 this.echo("wait_for_output timed out!");
174 174 });
175 175 };
176 176
177 177 casper.wait_for_widget = function (widget_info) {
178 178 // wait for a widget msg que to reach 0
179 179 //
180 180 // Parameters
181 181 // ----------
182 182 // widget_info : object
183 183 // Object which contains info related to the widget. The model_id property
184 184 // is used to identify the widget.
185 185
186 186 // Clear the results of a previous query, if they exist. Make sure a
187 187 // dictionary exists to store the async results in.
188 188 this.thenEvaluate(function(model_id) {
189 189 if (window.pending_msgs === undefined) {
190 190 window.pending_msgs = {};
191 191 } else {
192 192 window.pending_msgs[model_id] = -1;
193 193 }
194 194 }, {model_id: widget_info.model_id});
195 195
196 196 // Wait for the pending messages to be 0.
197 197 this.waitFor(function () {
198 198 var pending = this.evaluate(function (model_id) {
199 199
200 200 // Get the model. Once the model is had, store it's pending_msgs
201 201 // count in the window's dictionary.
202 202 IPython.notebook.kernel.widget_manager.get_model(model_id)
203 203 .then(function(model) {
204 204 window.pending_msgs[model_id] = model.pending_msgs;
205 205 });
206 206
207 207 // Return the pending_msgs result.
208 208 return window.pending_msgs[model_id];
209 209 }, {model_id: widget_info.model_id});
210 210
211 211 if (pending === 0) {
212 212 return true;
213 213 } else {
214 214 return false;
215 215 }
216 216 });
217 217 };
218 218
219 219 casper.get_output_cell = function (cell_num, out_num) {
220 220 // return an output of a given cell
221 221 out_num = out_num || 0;
222 222 var result = casper.evaluate(function (c, o) {
223 223 var cell = IPython.notebook.get_cell(c);
224 224 return cell.output_area.outputs[o];
225 225 },
226 226 {c : cell_num, o : out_num});
227 227 if (!result) {
228 228 var num_outputs = casper.evaluate(function (c) {
229 229 var cell = IPython.notebook.get_cell(c);
230 230 return cell.output_area.outputs.length;
231 231 },
232 232 {c : cell_num});
233 233 this.test.assertTrue(false,
234 234 "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
235 235 );
236 236 } else {
237 237 return result;
238 238 }
239 239 };
240 240
241 241 casper.get_cells_length = function () {
242 242 // return the number of cells in the notebook
243 243 var result = casper.evaluate(function () {
244 244 return IPython.notebook.get_cells().length;
245 245 });
246 246 return result;
247 247 };
248 248
249 249 casper.set_cell_text = function(index, text){
250 250 // Set the text content of a cell.
251 251 this.evaluate(function (index, text) {
252 252 var cell = IPython.notebook.get_cell(index);
253 253 cell.set_text(text);
254 254 }, index, text);
255 255 };
256 256
257 257 casper.get_cell_text = function(index){
258 258 // Get the text content of a cell.
259 259 return this.evaluate(function (index) {
260 260 var cell = IPython.notebook.get_cell(index);
261 261 return cell.get_text();
262 262 }, index);
263 263 };
264 264
265 265 casper.insert_cell_at_bottom = function(cell_type){
266 266 // Inserts a cell at the bottom of the notebook
267 267 // Returns the new cell's index.
268 268 return this.evaluate(function (cell_type) {
269 269 var cell = IPython.notebook.insert_cell_at_bottom(cell_type);
270 270 return IPython.notebook.find_cell_index(cell);
271 271 }, cell_type);
272 272 };
273 273
274 274 casper.append_cell = function(text, cell_type) {
275 275 // Insert a cell at the bottom of the notebook and set the cells text.
276 276 // Returns the new cell's index.
277 277 var index = this.insert_cell_at_bottom(cell_type);
278 278 if (text !== undefined) {
279 279 this.set_cell_text(index, text);
280 280 }
281 281 return index;
282 282 };
283 283
284 284 casper.execute_cell = function(index, expect_failure){
285 285 // Asynchronously executes a cell by index.
286 286 // Returns the cell's index.
287 287
288 288 if (expect_failure === undefined) expect_failure = false;
289 289 var that = this;
290 290 this.then(function(){
291 291 that.evaluate(function (index) {
292 292 var cell = IPython.notebook.get_cell(index);
293 293 cell.execute();
294 294 }, index);
295 295 });
296 296 this.wait_for_idle();
297 297
298 298 this.then(function () {
299 299 var error = that.evaluate(function (index) {
300 300 var cell = IPython.notebook.get_cell(index);
301 301 var outputs = cell.output_area.outputs;
302 302 for (var i = 0; i < outputs.length; i++) {
303 303 if (outputs[i].output_type == 'error') {
304 304 return outputs[i];
305 305 }
306 306 }
307 307 return false;
308 308 }, index);
309 309 if (error === null) {
310 310 this.test.fail("Failed to check for error output");
311 311 }
312 312 if (expect_failure && error === false) {
313 313 this.test.fail("Expected error while running cell");
314 314 } else if (!expect_failure && error !== false) {
315 315 this.test.fail("Error running cell:\n" + error.traceback.join('\n'));
316 316 }
317 317 });
318 318 return index;
319 319 };
320 320
321 321 casper.execute_cell_then = function(index, then_callback, expect_failure) {
322 322 // Synchronously executes a cell by index.
323 323 // Optionally accepts a then_callback parameter. then_callback will get called
324 324 // when the cell has finished executing.
325 325 // Returns the cell's index.
326 326 var return_val = this.execute_cell(index, expect_failure);
327 327
328 328 this.wait_for_idle();
329 329
330 330 var that = this;
331 331 this.then(function(){
332 332 if (then_callback!==undefined) {
333 333 then_callback.apply(that, [index]);
334 334 }
335 335 });
336 336
337 337 return return_val;
338 338 };
339 339
340 casper.append_cell_execute_then = function(text, then_callback, expect_failure) {
341 // Append a code cell and execute it, optionally calling a then_callback
342 var c = this.append_cell(text);
343 return this.execute_cell_then(c, then_callback, expect_failure);
344 };
345
346 casper.assert_output_equals = function(text, output_text, message) {
347 // Append a code cell with the text, then assert the output is equal to output_text
348 this.append_cell_execute_then(text, function(index) {
349 this.test.assertEquals(this.get_output_cell(index).text.trim(), output_text, message);
350 });
351 };
352
340 353 casper.wait_for_element = function(index, selector){
341 354 // Utility function that allows us to easily wait for an element
342 355 // within a cell. Uses JQuery selector to look for the element.
343 356 var that = this;
344 357 this.waitFor(function() {
345 358 return that.cell_element_exists(index, selector);
346 359 });
347 360 };
348 361
349 362 casper.cell_element_exists = function(index, selector){
350 363 // Utility function that allows us to easily check if an element exists
351 364 // within a cell. Uses JQuery selector to look for the element.
352 365 return casper.evaluate(function (index, selector) {
353 366 var $cell = IPython.notebook.get_cell(index).element;
354 367 return $cell.find(selector).length > 0;
355 368 }, index, selector);
356 369 };
357 370
358 371 casper.cell_element_function = function(index, selector, function_name, function_args){
359 372 // Utility function that allows us to execute a jQuery function on an
360 373 // element within a cell.
361 374 return casper.evaluate(function (index, selector, function_name, function_args) {
362 375 var $cell = IPython.notebook.get_cell(index).element;
363 376 var $el = $cell.find(selector);
364 377 return $el[function_name].apply($el, function_args);
365 378 }, index, selector, function_name, function_args);
366 379 };
367 380
368 381 casper.validate_notebook_state = function(message, mode, cell_index) {
369 382 // Validate the entire dual mode state of the notebook. Make sure no more than
370 383 // one cell is selected, focused, in edit mode, etc...
371 384
372 385 // General tests.
373 386 this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
374 387 message + '; keyboard and notebook modes match');
375 388 // Is the selected cell the only cell that is selected?
376 389 if (cell_index!==undefined) {
377 390 this.test.assert(this.is_only_cell_selected(cell_index),
378 391 message + '; cell ' + cell_index + ' is the only cell selected');
379 392 }
380 393
381 394 // Mode specific tests.
382 395 if (mode==='command') {
383 396 // Are the notebook and keyboard manager in command mode?
384 397 this.test.assertEquals(this.get_keyboard_mode(), 'command',
385 398 message + '; in command mode');
386 399 // Make sure there isn't a single cell in edit mode.
387 400 this.test.assert(this.is_only_cell_edit(null),
388 401 message + '; all cells in command mode');
389 402 this.test.assert(this.is_cell_editor_focused(null),
390 403 message + '; no cell editors are focused while in command mode');
391 404
392 405 } else if (mode==='edit') {
393 406 // Are the notebook and keyboard manager in edit mode?
394 407 this.test.assertEquals(this.get_keyboard_mode(), 'edit',
395 408 message + '; in edit mode');
396 409 if (cell_index!==undefined) {
397 410 // Is the specified cell the only cell in edit mode?
398 411 this.test.assert(this.is_only_cell_edit(cell_index),
399 412 message + '; cell ' + cell_index + ' is the only cell in edit mode '+ this.cells_modes());
400 413 // Is the specified cell the only cell with a focused code mirror?
401 414 this.test.assert(this.is_cell_editor_focused(cell_index),
402 415 message + '; cell ' + cell_index + '\'s editor is appropriately focused');
403 416 }
404 417
405 418 } else {
406 419 this.test.assert(false, message + '; ' + mode + ' is an unknown mode');
407 420 }
408 421 };
409 422
410 423 casper.select_cell = function(index) {
411 424 // Select a cell in the notebook.
412 425 this.evaluate(function (i) {
413 426 IPython.notebook.select(i);
414 427 }, {i: index});
415 428 };
416 429
417 430 casper.click_cell_editor = function(index) {
418 431 // Emulate a click on a cell's editor.
419 432
420 433 // Code Mirror does not play nicely with emulated brower events.
421 434 // Instead of trying to emulate a click, here we run code similar to
422 435 // the code used in Code Mirror that handles the mousedown event on a
423 436 // region of codemirror that the user can focus.
424 437 this.evaluate(function (i) {
425 438 var cm = IPython.notebook.get_cell(i).code_mirror;
426 439 if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input)){
427 440 cm.display.input.focus();
428 441 }
429 442 }, {i: index});
430 443 };
431 444
432 445 casper.set_cell_editor_cursor = function(index, line_index, char_index) {
433 446 // Set the Code Mirror instance cursor's location.
434 447 this.evaluate(function (i, l, c) {
435 448 IPython.notebook.get_cell(i).code_mirror.setCursor(l, c);
436 449 }, {i: index, l: line_index, c: char_index});
437 450 };
438 451
439 452 casper.focus_notebook = function() {
440 453 // Focus the notebook div.
441 454 this.evaluate(function (){
442 455 $('#notebook').focus();
443 456 }, {});
444 457 };
445 458
446 459 casper.trigger_keydown = function() {
447 460 // Emulate a keydown in the notebook.
448 461 for (var i = 0; i < arguments.length; i++) {
449 462 this.evaluate(function (k) {
450 463 var element = $(document);
451 464 var event = IPython.keyboard.shortcut_to_event(k, 'keydown');
452 465 element.trigger(event);
453 466 }, {k: arguments[i]});
454 467 }
455 468 };
456 469
457 470 casper.get_keyboard_mode = function() {
458 471 // Get the mode of the keyboard manager.
459 472 return this.evaluate(function() {
460 473 return IPython.keyboard_manager.mode;
461 474 }, {});
462 475 };
463 476
464 477 casper.get_notebook_mode = function() {
465 478 // Get the mode of the notebook.
466 479 return this.evaluate(function() {
467 480 return IPython.notebook.mode;
468 481 }, {});
469 482 };
470 483
471 484 casper.get_cell = function(index) {
472 485 // Get a single cell.
473 486 //
474 487 // Note: Handles to DOM elements stored in the cell will be useless once in
475 488 // CasperJS context.
476 489 return this.evaluate(function(i) {
477 490 var cell = IPython.notebook.get_cell(i);
478 491 if (cell) {
479 492 return cell;
480 493 }
481 494 return null;
482 495 }, {i : index});
483 496 };
484 497
485 498 casper.is_cell_editor_focused = function(index) {
486 499 // Make sure a cell's editor is the only editor focused on the page.
487 500 return this.evaluate(function(i) {
488 501 var focused_textarea = $('#notebook .CodeMirror-focused textarea');
489 502 if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; }
490 503 if (i === null) {
491 504 return focused_textarea.length === 0;
492 505 } else {
493 506 var cell = IPython.notebook.get_cell(i);
494 507 if (cell) {
495 508 return cell.code_mirror.getInputField() == focused_textarea[0];
496 509 }
497 510 }
498 511 return false;
499 512 }, {i : index});
500 513 };
501 514
502 515 casper.is_only_cell_selected = function(index) {
503 516 // Check if a cell is the only cell selected.
504 517 // Pass null as the index to check if no cells are selected.
505 518 return this.is_only_cell_on(index, 'selected', 'unselected');
506 519 };
507 520
508 521 casper.is_only_cell_edit = function(index) {
509 522 // Check if a cell is the only cell in edit mode.
510 523 // Pass null as the index to check if all of the cells are in command mode.
511 524 var cells_length = this.get_cells_length();
512 525 for (var j = 0; j < cells_length; j++) {
513 526 if (j === index) {
514 527 if (!this.cell_mode_is(j, 'edit')) {
515 528 return false;
516 529 }
517 530 } else {
518 531 if (this.cell_mode_is(j, 'edit')) {
519 532 return false;
520 533 }
521 534 }
522 535 }
523 536 return true;
524 537 };
525 538
526 539 casper.is_only_cell_on = function(i, on_class, off_class) {
527 540 // Check if a cell is the only cell with the `on_class` DOM class applied to it.
528 541 // All of the other cells are checked for the `off_class` DOM class.
529 542 // Pass null as the index to check if all of the cells have the `off_class`.
530 543 var cells_length = this.get_cells_length();
531 544 for (var j = 0; j < cells_length; j++) {
532 545 if (j === i) {
533 546 if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) {
534 547 return false;
535 548 }
536 549 } else {
537 550 if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) {
538 551 return false;
539 552 }
540 553 }
541 554 }
542 555 return true;
543 556 };
544 557
545 558 casper.cells_modes = function(){
546 559 return this.evaluate(function(){
547 560 return IPython.notebook.get_cells().map(function(x,c){return x.mode})
548 561 }, {});
549 562 };
550 563
551 564 casper.cell_mode_is = function(index, mode) {
552 565 // Check if a cell is in a specific mode
553 566 return this.evaluate(function(i, m) {
554 567 var cell = IPython.notebook.get_cell(i);
555 568 if (cell) {
556 569 return cell.mode === m;
557 570 }
558 571 return false;
559 572 }, {i : index, m: mode});
560 573 };
561 574
562 575
563 576 casper.cell_has_class = function(index, classes) {
564 577 // Check if a cell has a class.
565 578 return this.evaluate(function(i, c) {
566 579 var cell = IPython.notebook.get_cell(i);
567 580 if (cell) {
568 581 return cell.element.hasClass(c);
569 582 }
570 583 return false;
571 584 }, {i : index, c: classes});
572 585 };
573 586
574 587 casper.is_cell_rendered = function (index) {
575 588 return this.evaluate(function(i) {
576 589 return !!IPython.notebook.get_cell(i).rendered;
577 590 }, {i:index});
578 591 };
579 592
580 593 casper.assert_colors_equal = function (hex_color, local_color, msg) {
581 594 // Tests to see if two colors are equal.
582 595 //
583 596 // Parameters
584 597 // hex_color: string
585 598 // Hexadecimal color code, with or without preceeding hash character.
586 599 // local_color: string
587 600 // Local color representation. Can either be hexadecimal (default for
588 601 // phantom) or rgb (default for slimer).
589 602
590 603 // Remove parentheses, hashes, semi-colons, and space characters.
591 604 hex_color = hex_color.replace(/[\(\); #]/, '');
592 605 local_color = local_color.replace(/[\(\); #]/, '');
593 606
594 607 // If the local color is rgb, clean it up and replace
595 608 if (local_color.substr(0,3).toLowerCase() == 'rgb') {
596 609 var components = local_color.substr(3).split(',');
597 610 local_color = '';
598 611 for (var i = 0; i < components.length; i++) {
599 612 var part = parseInt(components[i]).toString(16);
600 613 while (part.length < 2) part = '0' + part;
601 614 local_color += part;
602 615 }
603 616 }
604 617
605 618 this.test.assertEquals(hex_color.toUpperCase(), local_color.toUpperCase(), msg);
606 619 };
607 620
608 621 casper.notebook_test = function(test) {
609 622 // Wrap a notebook test to reduce boilerplate.
610 623 this.open_new_notebook();
611 624
612 625 // Echo whether or not we are running this test using SlimerJS
613 626 if (this.evaluate(function(){
614 627 return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
615 628 })) {
616 629 console.log('This test is running in SlimerJS.');
617 630 this.slimerjs = true;
618 631 }
619 632
620 633 // Make sure to remove the onbeforeunload callback. This callback is
621 634 // responsible for the "Are you sure you want to quit?" type messages.
622 635 // PhantomJS ignores these prompts, SlimerJS does not which causes hangs.
623 636 this.then(function(){
624 637 this.evaluate(function(){
625 638 window.onbeforeunload = function(){};
626 639 });
627 640 });
628 641
629 642 this.then(test);
630 643
631 644 // Kill the kernel and delete the notebook.
632 645 this.shutdown_current_kernel();
633 646 // This is still broken but shouldn't be a problem for now.
634 647 // this.delete_current_notebook();
635 648
636 649 // This is required to clean up the page we just finished with. If we don't call this
637 650 // casperjs will leak file descriptors of all the open WebSockets in that page. We
638 651 // have to set this.page=null so that next time casper.start runs, it will create a
639 652 // new page from scratch.
640 653 this.then(function () {
641 654 this.page.close();
642 655 this.page = null;
643 656 });
644 657
645 658 // Run the browser automation.
646 659 this.run(function() {
647 660 this.test.done();
648 661 });
649 662 };
650 663
651 664 casper.wait_for_dashboard = function () {
652 665 // Wait for the dashboard list to load.
653 666 casper.waitForSelector('.list_item');
654 667 };
655 668
656 669 casper.open_dashboard = function () {
657 670 // Start casper by opening the dashboard page.
658 671 var baseUrl = this.get_notebook_server();
659 672 this.start(baseUrl);
660 673 this.waitFor(this.page_loaded);
661 674 this.wait_for_dashboard();
662 675 };
663 676
664 677 casper.dashboard_test = function (test) {
665 678 // Open the dashboard page and run a test.
666 679 this.open_dashboard();
667 680 this.then(test);
668 681
669 682 this.then(function () {
670 683 this.page.close();
671 684 this.page = null;
672 685 });
673 686
674 687 // Run the browser automation.
675 688 this.run(function() {
676 689 this.test.done();
677 690 });
678 691 };
679 692
680 693 // note that this will only work for UNIQUE events -- if you want to
681 694 // listen for the same event twice, this will not work!
682 695 casper.event_test = function (name, events, action, timeout) {
683 696
684 697 // set up handlers to listen for each of the events
685 698 this.thenEvaluate(function (events) {
686 699 var make_handler = function (event) {
687 700 return function () {
688 701 IPython._events_triggered.push(event);
689 702 IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
690 703 delete IPython._event_handlers[event];
691 704 };
692 705 };
693 706 IPython._event_handlers = {};
694 707 IPython._events_triggered = [];
695 708 for (var i=0; i < events.length; i++) {
696 709 IPython._event_handlers[events[i]] = make_handler(events[i]);
697 710 IPython.notebook.events.on(events[i], IPython._event_handlers[events[i]]);
698 711 }
699 712 }, [events]);
700 713
701 714 // execute the requested action
702 715 this.then(action);
703 716
704 717 // wait for all the events to be triggered
705 718 this.waitFor(function () {
706 719 return this.evaluate(function (events) {
707 720 return IPython._events_triggered.length >= events.length;
708 721 }, [events]);
709 722 }, undefined, undefined, timeout);
710 723
711 724 // test that the events were triggered in the proper order
712 725 this.then(function () {
713 726 var triggered = this.evaluate(function () {
714 727 return IPython._events_triggered;
715 728 });
716 729 var handlers = this.evaluate(function () {
717 730 return Object.keys(IPython._event_handlers);
718 731 });
719 732 this.test.assertEquals(triggered.length, events.length, name + ': ' + events.length + ' events were triggered');
720 733 this.test.assertEquals(handlers.length, 0, name + ': all handlers triggered');
721 734 for (var i=0; i < events.length; i++) {
722 735 this.test.assertEquals(triggered[i], events[i], name + ': ' + events[i] + ' was triggered');
723 736 }
724 737 });
725 738
726 739 // turn off any remaining event listeners
727 740 this.thenEvaluate(function () {
728 741 for (var event in IPython._event_handlers) {
729 742 IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
730 743 delete IPython._event_handlers[event];
731 744 }
732 745 });
733 746 };
734 747
735 748 casper.options.waitTimeout=10000;
736 749 casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
737 750 this.echo("Timeout for " + casper.get_notebook_server());
738 751 this.echo("Is the notebook server running?");
739 752 });
740 753
741 754 casper.print_log = function () {
742 755 // Pass `console.log` calls from page JS to casper.
743 756 this.on('remote.message', function(msg) {
744 757 this.echo('Remote message caught: ' + msg);
745 758 });
746 759 };
747 760
748 761 casper.on("page.error", function onError(msg, trace) {
749 762 // show errors in the browser
750 763 this.echo("Page Error");
751 764 this.echo(" Message: " + msg.split('\n').join('\n '));
752 765 this.echo(" Call stack:");
753 766 var local_path = this.get_notebook_server();
754 767 for (var i = 0; i < trace.length; i++) {
755 768 var frame = trace[i];
756 769 var file = frame.file;
757 770 // shorten common phantomjs evaluate url
758 771 // this will have a different value on slimerjs
759 772 if (file === "phantomjs://webpage.evaluate()") {
760 773 file = "evaluate";
761 774 }
762 775 // remove the version tag from the path
763 776 file = file.replace(/(\?v=[0-9abcdef]+)/, '');
764 777 // remove the local address from the beginning of the path
765 778 if (file.indexOf(local_path) === 0) {
766 779 file = file.substr(local_path.length);
767 780 }
768 781 var frame_text = (frame.function.length > 0) ? " in " + frame.function : "";
769 782 this.echo(" line " + frame.line + " of " + file + frame_text);
770 783 }
771 784 });
772 785
773 786
774 787 casper.capture_log = function () {
775 788 // show captured errors
776 789 var captured_log = [];
777 790 var seen_errors = 0;
778 791 this.on('remote.message', function(msg) {
779 792 captured_log.push(msg);
780 793 });
781 794
782 795 var that = this;
783 796 this.test.on("test.done", function (result) {
784 797 // test.done runs per-file,
785 798 // but suiteResults is per-suite (directory)
786 799 var current_errors;
787 800 if (this.suiteResults) {
788 801 // casper 1.1 has suiteResults
789 802 current_errors = this.suiteResults.countErrors() + this.suiteResults.countFailed();
790 803 } else {
791 804 // casper 1.0 has testResults instead
792 805 current_errors = this.testResults.failed;
793 806 }
794 807
795 808 if (current_errors > seen_errors && captured_log.length > 0) {
796 809 casper.echo("\nCaptured console.log:");
797 810 for (var i = 0; i < captured_log.length; i++) {
798 811 var output = String(captured_log[i]).split('\n');
799 812 for (var j = 0; j < output.length; j++) {
800 813 casper.echo(" " + output[j]);
801 814 }
802 815 }
803 816 }
804 817
805 818 seen_errors = current_errors;
806 819 captured_log = [];
807 820 });
808 821 };
809 822
810 823 casper.interact = function() {
811 824 // Start an interactive Javascript console.
812 825 var system = require('system');
813 826 system.stdout.writeLine('JS interactive console.');
814 827 system.stdout.writeLine('Type `exit` to quit.');
815 828
816 829 function read_line() {
817 830 system.stdout.writeLine('JS: ');
818 831 var line = system.stdin.readLine();
819 832 return line;
820 833 }
821 834
822 835 var input = read_line();
823 836 while (input.trim() != 'exit') {
824 837 var output = this.evaluate(function(code) {
825 838 return String(eval(code));
826 839 }, {code: input});
827 840 system.stdout.writeLine('\nOut: ' + output);
828 841 input = read_line();
829 842 }
830 843 };
831 844
832 845 casper.capture_log();
@@ -1,191 +1,309 b''
1 1 var xor = function (a, b) {return !a ^ !b;};
2 2 var isArray = function (a) {
3 3 try {
4 4 return Object.toString.call(a) === "[object Array]" || Object.toString.call(a) === "[object RuntimeArray]";
5 5 } catch (e) {
6 6 return Array.isArray(a);
7 7 }
8 8 };
9 9 var recursive_compare = function(a, b) {
10 10 // Recursively compare two objects.
11 11 var same = true;
12 12 same = same && !xor(a instanceof Object || typeof a == 'object', b instanceof Object || typeof b == 'object');
13 13 same = same && !xor(isArray(a), isArray(b));
14 14
15 15 if (same) {
16 16 if (a instanceof Object) {
17 17 var key;
18 18 for (key in a) {
19 19 if (a.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
20 20 same = false;
21 21 break;
22 22 }
23 23 }
24 24 for (key in b) {
25 25 if (b.hasOwnProperty(key) && !recursive_compare(a[key], b[key])) {
26 26 same = false;
27 27 break;
28 28 }
29 29 }
30 30 } else {
31 31 return a === b;
32 32 }
33 33 }
34 34
35 35 return same;
36 36 };
37 37
38 38 // Test the widget framework.
39 39 casper.notebook_test(function () {
40 40 var index;
41 41
42 42 index = this.append_cell(
43 'from IPython.html import widgets\n' +
44 'from IPython.display import display, clear_output\n' +
45 'print("Success")');
43 ['from IPython.html import widgets',
44 'from IPython.display import display, clear_output',
45 'print("Success")'].join('\n'));
46 46 this.execute_cell_then(index);
47 47
48 48 this.then(function () {
49
50 // Functions that can be used to test the packing and unpacking APIs
51 var that = this;
52 var test_pack = function (input) {
53 var output = that.evaluate(function(input) {
54 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
55 var results = model._pack_models(input);
56 return results;
57 }, {input: input});
58 that.test.assert(recursive_compare(input, output),
59 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
60 };
61 var test_unpack = function (input) {
62 that.thenEvaluate(function(input) {
63 window.results = undefined;
64 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
65 model._unpack_models(input).then(function(results) {
66 window.results = results;
67 });
68 }, {input: input});
69
70 that.waitFor(function check() {
71 return that.evaluate(function() {
72 return window.results;
73 });
74 });
75
76 that.then(function() {
77 var results = that.evaluate(function() {
78 return window.results;
79 });
80 that.test.assert(recursive_compare(input, results),
81 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
82 });
83 };
84 var test_packing = function(input) {
85 test_pack(input);
86 test_unpack(input);
87 };
88
89 test_packing({0: 'hi', 1: 'bye'});
90 test_packing(['hi', 'bye']);
91 test_packing(['hi', 5]);
92 test_packing(['hi', '5']);
93 test_packing([1.0, 0]);
94 test_packing([1.0, false]);
95 test_packing([1, false]);
96 test_packing([1, false, {a: 'hi'}]);
97 test_packing([1, false, ['hi']]);
98 test_packing([String('hi'), Date("Thu Nov 13 2014 13:46:21 GMT-0500")])
99
100 49 // Test multi-set, single touch code. First create a custom widget.
101 50 this.thenEvaluate(function() {
102 51 var MultiSetView = IPython.DOMWidgetView.extend({
103 52 render: function(){
104 53 this.model.set('a', 1);
105 54 this.model.set('b', 2);
106 55 this.model.set('c', 3);
107 56 this.touch();
108 57 },
109 58 });
110 59 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
111 60 }, {});
112 61 });
113 62
114 63 // Try creating the multiset widget, verify that sets the values correctly.
115 64 var multiset = {};
116 multiset.index = this.append_cell(
117 'from IPython.utils.traitlets import Unicode, CInt\n' +
118 'class MultiSetWidget(widgets.Widget):\n' +
119 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
120 ' a = CInt(0, sync=True)\n' +
121 ' b = CInt(0, sync=True)\n' +
122 ' c = CInt(0, sync=True)\n' +
123 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
124 ' def set_state(self, sync_data):\n' +
125 ' widgets.Widget.set_state(self, sync_data)\n'+
126 ' self.d = len(sync_data)\n' +
127 'multiset = MultiSetWidget()\n' +
128 'display(multiset)\n' +
129 'print(multiset.model_id)');
65 multiset.index = this.append_cell([
66 'from IPython.utils.traitlets import Unicode, CInt',
67 'class MultiSetWidget(widgets.Widget):',
68 ' _view_name = Unicode("MultiSetView", sync=True)',
69 ' a = CInt(0, sync=True)',
70 ' b = CInt(0, sync=True)',
71 ' c = CInt(0, sync=True)',
72 ' d = CInt(-1, sync=True)', // See if it sends a full state.
73 ' def set_state(self, sync_data):',
74 ' widgets.Widget.set_state(self, sync_data)',
75 ' self.d = len(sync_data)',
76 'multiset = MultiSetWidget()',
77 'display(multiset)',
78 'print(multiset.model_id)'].join('\n'));
130 79 this.execute_cell_then(multiset.index, function(index) {
131 80 multiset.model_id = this.get_output_cell(index).text.trim();
132 81 });
133 82
134 83 this.wait_for_widget(multiset);
135 84
136 85 index = this.append_cell(
137 86 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
138 87 this.execute_cell_then(index, function(index) {
139 88 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
140 89 'Multiple model.set calls and one view.touch update state in back-end.');
141 90 });
142 91
143 92 index = this.append_cell(
144 93 'print("%d" % (multiset.d))');
145 94 this.execute_cell_then(index, function(index) {
146 95 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
147 96 'Multiple model.set calls sent a partial state.');
148 97 });
149 98
150 99 var textbox = {};
151 throttle_index = this.append_cell(
152 'import time\n' +
153 'textbox = widgets.Text()\n' +
154 'display(textbox)\n' +
155 'textbox._dom_classes = ["my-throttle-textbox"]\n' +
156 'def handle_change(name, old, new):\n' +
157 ' display(len(new))\n' +
158 ' time.sleep(0.5)\n' +
159 'textbox.on_trait_change(handle_change, "value")\n' +
160 'print(textbox.model_id)');
100 throttle_index = this.append_cell([
101 'import time',
102 'textbox = widgets.Text()',
103 'display(textbox)',
104 'textbox._dom_classes = ["my-throttle-textbox"]',
105 'def handle_change(name, old, new):',
106 ' display(len(new))',
107 ' time.sleep(0.5)',
108 'textbox.on_trait_change(handle_change, "value")',
109 'print(textbox.model_id)'].join('\n'));
161 110 this.execute_cell_then(throttle_index, function(index){
162 111 textbox.model_id = this.get_output_cell(index).text.trim();
163 112
164 113 this.test.assert(this.cell_element_exists(index,
165 114 '.widget-area .widget-subarea'),
166 115 'Widget subarea exists.');
167 116
168 117 this.test.assert(this.cell_element_exists(index,
169 118 '.my-throttle-textbox'), 'Textbox exists.');
170 119
171 120 // Send 20 characters
172 this.sendKeys('.my-throttle-textbox input', '....................');
121 this.sendKeys('.my-throttle-textbox input', '12345678901234567890');
173 122 });
174 123
175 124 this.wait_for_widget(textbox);
176 125
177 126 this.then(function () {
178 127 var outputs = this.evaluate(function(i) {
179 128 return IPython.notebook.get_cell(i).output_area.outputs;
180 129 }, {i : throttle_index});
181 130
182 131 // Only 4 outputs should have printed, but because of timing, sometimes
183 132 // 5 outputs will print. All we need to do is verify num outputs <= 5
184 133 // because that is much less than 20.
185 134 this.test.assert(outputs.length <= 5, 'Messages throttled.');
186 135
187 136 // We also need to verify that the last state sent was correct.
188 137 var last_state = outputs[outputs.length-1].data['text/plain'];
189 138 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
190 139 });
140
141
142 this.thenEvaluate(function() {
143 define('TestWidget', ['widgets/js/widget', 'base/js/utils', 'underscore'], function(widget, utils, _) {
144 var floatArray = {
145 deserialize: function (value, model) {
146 if (value===null) {return null;}
147 // DataView -> float64 typed array
148 return new Float64Array(value.buffer);
149 },
150 // serialization automatically handled since the
151 // attribute is an ArrayBuffer view
152 };
153
154 var floatList = {
155 deserialize: function (value, model) {
156 // list of floats -> list of strings
157 return value.map(function(x) {return x.toString()});
158 },
159 serialize: function(value, model) {
160 // list of strings -> list of floats
161 return value.map(function(x) {return parseFloat(x);})
162 }
163 };
164
165 var TestWidgetModel = widget.WidgetModel.extend({}, {
166 serializers: _.extend({
167 array_list: floatList,
168 array_binary: floatArray
169 }, widget.WidgetModel.serializers)
170 });
171
172 var TestWidgetView = widget.DOMWidgetView.extend({
173 render: function () {
174 this.listenTo(this.model, 'msg:custom', this.handle_msg);
175 },
176 handle_msg: function(content, buffers) {
177 this.msg = [content, buffers];
178 }
179 });
180
181 return {TestWidgetModel: TestWidgetModel, TestWidgetView: TestWidgetView};
182 });
183 });
184
185 var testwidget = {};
186 this.append_cell_execute_then([
187 'from IPython.html import widgets',
188 'from IPython.utils.traitlets import Unicode, Instance, List',
189 'from IPython.display import display',
190 'from array import array',
191 'def _array_to_memoryview(x):',
192 ' if x is None: return None',
193 ' try:',
194 ' y = memoryview(x)',
195 ' except TypeError:',
196 ' # in python 2, arrays do not support the new buffer protocol',
197 ' y = memoryview(buffer(x))',
198 ' return y',
199 'def _memoryview_to_array(x):',
200 ' if x is None: return None',
201 ' return array("d", x.tobytes())',
202 'arrays_binary = {',
203 ' "from_json": _memoryview_to_array,',
204 ' "to_json": _array_to_memoryview',
205 '}',
206 '',
207 'def _array_to_list(x):',
208 ' return list(x)',
209 'def _list_to_array(x):',
210 ' return array("d",x)',
211 'arrays_list = {',
212 ' "from_json": _list_to_array,',
213 ' "to_json": _array_to_list',
214 '}',
215 '',
216 'class TestWidget(widgets.DOMWidget):',
217 ' _model_module = Unicode("TestWidget", sync=True)',
218 ' _model_name = Unicode("TestWidgetModel", sync=True)',
219 ' _view_module = Unicode("TestWidget", sync=True)',
220 ' _view_name = Unicode("TestWidgetView", sync=True)',
221 ' array_binary = Instance(array, allow_none=True, sync=True, **arrays_binary)',
222 ' array_list = Instance(array, args=("d", [3.0]), allow_none=False, sync=True, **arrays_list)',
223 ' msg = {}',
224 ' def __init__(self, **kwargs):',
225 ' super(widgets.DOMWidget, self).__init__(**kwargs)',
226 ' self.on_msg(self._msg)',
227 ' def _msg(self, _, content, buffers):',
228 ' self.msg = [content, buffers]',
229 'x=TestWidget()',
230 'display(x)',
231 'print(x.model_id)'].join('\n'), function(index){
232 testwidget.index = index;
233 testwidget.model_id = this.get_output_cell(index).text.trim();
234 });
235 this.wait_for_widget(testwidget);
236
237
238 this.append_cell_execute_then('x.array_list = array("d", [1.5, 2.0, 3.1])');
239 this.wait_for_widget(testwidget);
240 this.then(function() {
241 var result = this.evaluate(function(index) {
242 var v = IPython.notebook.get_cell(index).widget_views[0];
243 var result = v.model.get('array_list');
244 var z = result.slice();
245 z[0]+="1234";
246 z[1]+="5678";
247 v.model.set('array_list', z);
248 v.touch();
249 return result;
250 }, testwidget.index);
251 this.test.assertEquals(result, ["1.5", "2", "3.1"], "JSON custom serializer kernel -> js");
252 });
253
254 this.assert_output_equals('print(x.array_list.tolist() == [1.51234, 25678.0, 3.1])',
255 'True', 'JSON custom serializer js -> kernel');
256
257 if (this.slimerjs) {
258 this.append_cell_execute_then("x.array_binary=array('d', [1.5,2.5,5])", function() {
259 this.evaluate(function(index) {
260 var v = IPython.notebook.get_cell(index).widget_views[0];
261 var z = v.model.get('array_binary');
262 z[0]*=3;
263 z[1]*=3;
264 z[2]*=3;
265 // we set to null so that we recognize the change
266 // when we set data back to z
267 v.model.set('array_binary', null);
268 v.model.set('array_binary', z);
269 v.touch();
270 }, textwidget.index);
271 });
272 this.wait_for_widget(testwidget);
273 this.assert_output_equals('x.array_binary.tolist() == [4.5, 7.5, 15.0]',
274 'True\n', 'Binary custom serializer js -> kernel')
275
276 this.append_cell_execute_then('x.send("some content", [memoryview(b"binarycontent"), memoryview("morecontent")])');
277 this.wait_for_widget(testwidget);
278
279 this.then(function() {
280 var result = this.evaluate(function(index) {
281 var v = IPython.notebook.get_cell(index).widget_views[0];
282 var d = new TextDecoder('utf-8');
283 return {text: v.msg[0],
284 binary0: d.decode(v.msg[1][0]),
285 binary1: d.decode(v.msg[1][1])};
286 }, testwidget.index);
287 this.test.assertEquals(result, {text: 'some content',
288 binary0: 'binarycontent',
289 binary1: 'morecontent'},
290 "Binary widget messages kernel -> js");
291 });
292
293 this.then(function() {
294 this.evaluate(function(index) {
295 var v = IPython.notebook.get_cell(index).widget_views[0];
296 v.send('content back', [new Uint8Array([1,2,3,4]), new Float64Array([2.1828, 3.14159])])
297 }, testwidget.index);
298 });
299 this.wait_for_widget(testwidget);
300 this.assert_output_equals([
301 'all([x.msg[0] == "content back",',
302 ' x.msg[1][0].tolist() == [1,2,3,4],',
303 ' array("d", x.msg[1][1].tobytes()).tolist() == [2.1828, 3.14159]])'].join('\n'),
304 'True', 'Binary buffers message js -> kernel');
305 } else {
306 console.log("skipping binary websocket tests on phantomjs");
307 }
308
191 309 });
@@ -1,497 +1,497 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16 import collections
17 17
18 18 from IPython.core.getipython import get_ipython
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.importstring import import_item
22 22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 24 from IPython.utils.py3compat import string_types
25 25 from .trait_types import Color
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Classes
29 29 #-----------------------------------------------------------------------------
30 30 class CallbackDispatcher(LoggingConfigurable):
31 31 """A structure for registering and running callbacks"""
32 32 callbacks = List()
33 33
34 34 def __call__(self, *args, **kwargs):
35 35 """Call all of the registered callbacks."""
36 36 value = None
37 37 for callback in self.callbacks:
38 38 try:
39 39 local_value = callback(*args, **kwargs)
40 40 except Exception as e:
41 41 ip = get_ipython()
42 42 if ip is None:
43 43 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
44 44 else:
45 45 ip.showtraceback()
46 46 else:
47 47 value = local_value if local_value is not None else value
48 48 return value
49 49
50 50 def register_callback(self, callback, remove=False):
51 51 """(Un)Register a callback
52 52
53 53 Parameters
54 54 ----------
55 55 callback: method handle
56 56 Method to be registered or unregistered.
57 57 remove=False: bool
58 58 Whether to unregister the callback."""
59 59
60 60 # (Un)Register the callback.
61 61 if remove and callback in self.callbacks:
62 62 self.callbacks.remove(callback)
63 63 elif not remove and callback not in self.callbacks:
64 64 self.callbacks.append(callback)
65 65
66 66 def _show_traceback(method):
67 67 """decorator for showing tracebacks in IPython"""
68 68 def m(self, *args, **kwargs):
69 69 try:
70 70 return(method(self, *args, **kwargs))
71 71 except Exception as e:
72 72 ip = get_ipython()
73 73 if ip is None:
74 74 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
75 75 else:
76 76 ip.showtraceback()
77 77 return m
78 78
79 79
80 80 def register(key=None):
81 81 """Returns a decorator registering a widget class in the widget registry.
82 82 If no key is provided, the class name is used as a key. A key is
83 83 provided for each core IPython widget so that the frontend can use
84 84 this key regardless of the language of the kernel"""
85 85 def wrap(widget):
86 86 l = key if key is not None else widget.__module__ + widget.__name__
87 87 Widget.widget_types[l] = widget
88 88 return widget
89 89 return wrap
90 90
91 91
92 92 class Widget(LoggingConfigurable):
93 93 #-------------------------------------------------------------------------
94 94 # Class attributes
95 95 #-------------------------------------------------------------------------
96 96 _widget_construction_callback = None
97 97 widgets = {}
98 98 widget_types = {}
99 99
100 100 @staticmethod
101 101 def on_widget_constructed(callback):
102 102 """Registers a callback to be called when a widget is constructed.
103 103
104 104 The callback must have the following signature:
105 105 callback(widget)"""
106 106 Widget._widget_construction_callback = callback
107 107
108 108 @staticmethod
109 109 def _call_widget_constructed(widget):
110 110 """Static method, called when a widget is constructed."""
111 111 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
112 112 Widget._widget_construction_callback(widget)
113 113
114 114 @staticmethod
115 115 def handle_comm_opened(comm, msg):
116 116 """Static method, called when a widget is constructed."""
117 117 widget_class = import_item(msg['content']['data']['widget_class'])
118 118 widget = widget_class(comm=comm)
119 119
120 120
121 121 #-------------------------------------------------------------------------
122 122 # Traits
123 123 #-------------------------------------------------------------------------
124 124 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
125 125 in which to find _model_name. If empty, look in the global registry.""")
126 126 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
127 127 registered in the front-end to create and sync this widget with.""")
128 128 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
129 129 If empty, look in the global registry.""", sync=True)
130 130 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
131 131 to use to represent the widget.""", sync=True)
132 132 comm = Instance('IPython.kernel.comm.Comm', allow_none=True)
133 133
134 134 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
135 135 front-end can send before receiving an idle msg from the back-end.""")
136 136
137 137 version = Int(0, sync=True, help="""Widget's version""")
138 138 keys = List()
139 139 def _keys_default(self):
140 140 return [name for name in self.traits(sync=True)]
141 141
142 142 _property_lock = Tuple((None, None))
143 143 _send_state_lock = Int(0)
144 144 _states_to_send = Set()
145 145 _display_callbacks = Instance(CallbackDispatcher, ())
146 146 _msg_callbacks = Instance(CallbackDispatcher, ())
147 147
148 148 #-------------------------------------------------------------------------
149 149 # (Con/de)structor
150 150 #-------------------------------------------------------------------------
151 151 def __init__(self, **kwargs):
152 152 """Public constructor"""
153 153 self._model_id = kwargs.pop('model_id', None)
154 154 super(Widget, self).__init__(**kwargs)
155 155
156 156 Widget._call_widget_constructed(self)
157 157 self.open()
158 158
159 159 def __del__(self):
160 160 """Object disposal"""
161 161 self.close()
162 162
163 163 #-------------------------------------------------------------------------
164 164 # Properties
165 165 #-------------------------------------------------------------------------
166 166
167 167 def open(self):
168 168 """Open a comm to the frontend if one isn't already open."""
169 169 if self.comm is None:
170 170 args = dict(target_name='ipython.widget',
171 171 data={'model_name': self._model_name,
172 172 'model_module': self._model_module})
173 173 if self._model_id is not None:
174 174 args['comm_id'] = self._model_id
175 175 self.comm = Comm(**args)
176 176
177 177 def _comm_changed(self, name, new):
178 178 """Called when the comm is changed."""
179 179 if new is None:
180 180 return
181 181 self._model_id = self.model_id
182 182
183 183 self.comm.on_msg(self._handle_msg)
184 184 Widget.widgets[self.model_id] = self
185 185
186 186 # first update
187 187 self.send_state()
188 188
189 189 @property
190 190 def model_id(self):
191 191 """Gets the model id of this widget.
192 192
193 193 If a Comm doesn't exist yet, a Comm will be created automagically."""
194 194 return self.comm.comm_id
195 195
196 196 #-------------------------------------------------------------------------
197 197 # Methods
198 198 #-------------------------------------------------------------------------
199 199
200 200 def close(self):
201 201 """Close method.
202 202
203 203 Closes the underlying comm.
204 204 When the comm is closed, all of the widget views are automatically
205 205 removed from the front-end."""
206 206 if self.comm is not None:
207 207 Widget.widgets.pop(self.model_id, None)
208 208 self.comm.close()
209 209 self.comm = None
210 210
211 211 def send_state(self, key=None):
212 212 """Sends the widget state, or a piece of it, to the front-end.
213 213
214 214 Parameters
215 215 ----------
216 216 key : unicode, or iterable (optional)
217 217 A single property's name or iterable of property names to sync with the front-end.
218 218 """
219 self._send({
220 "method" : "update",
221 "state" : self.get_state(key=key)
222 })
219 state, buffer_keys, buffers = self.get_state(key=key)
220 msg = {"method": "update", "state": state}
221 if buffer_keys:
222 msg['buffers'] = buffer_keys
223 self._send(msg, buffers=buffers)
223 224
224 225 def get_state(self, key=None):
225 226 """Gets the widget state, or a piece of it.
226 227
227 228 Parameters
228 229 ----------
229 230 key : unicode or iterable (optional)
230 231 A single property's name or iterable of property names to get.
232
233 Returns
234 -------
235 state : dict of states
236 buffer_keys : list of strings
237 the values that are stored in buffers
238 buffers : list of binary memoryviews
239 values to transmit in binary
240 metadata : dict
241 metadata for each field: {key: metadata}
231 242 """
232 243 if key is None:
233 244 keys = self.keys
234 245 elif isinstance(key, string_types):
235 246 keys = [key]
236 247 elif isinstance(key, collections.Iterable):
237 248 keys = key
238 249 else:
239 250 raise ValueError("key must be a string, an iterable of keys, or None")
240 251 state = {}
252 buffers = []
253 buffer_keys = []
241 254 for k in keys:
242 255 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 256 value = getattr(self, k)
244 state[k] = f(value)
245 return state
257 serialized = f(value)
258 if isinstance(serialized, memoryview):
259 buffers.append(serialized)
260 buffer_keys.append(k)
261 else:
262 state[k] = serialized
263 return state, buffer_keys, buffers
246 264
247 265 def set_state(self, sync_data):
248 266 """Called when a state is received from the front-end."""
249 267 for name in self.keys:
250 268 if name in sync_data:
251 269 json_value = sync_data[name]
252 270 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
253 271 with self._lock_property(name, json_value):
254 272 setattr(self, name, from_json(json_value))
255 273
256 def send(self, content):
274 def send(self, content, buffers=None):
257 275 """Sends a custom msg to the widget model in the front-end.
258 276
259 277 Parameters
260 278 ----------
261 279 content : dict
262 280 Content of the message to send.
281 buffers : list of binary buffers
282 Binary buffers to send with message
263 283 """
264 self._send({"method": "custom", "content": content})
284 self._send({"method": "custom", "content": content}, buffers=buffers)
265 285
266 286 def on_msg(self, callback, remove=False):
267 287 """(Un)Register a custom msg receive callback.
268 288
269 289 Parameters
270 290 ----------
271 291 callback: callable
272 callback will be passed two arguments when a message arrives::
292 callback will be passed three arguments when a message arrives::
273 293
274 callback(widget, content)
294 callback(widget, content, buffers)
275 295
276 296 remove: bool
277 297 True if the callback should be unregistered."""
278 298 self._msg_callbacks.register_callback(callback, remove=remove)
279 299
280 300 def on_displayed(self, callback, remove=False):
281 301 """(Un)Register a widget displayed callback.
282 302
283 303 Parameters
284 304 ----------
285 305 callback: method handler
286 306 Must have a signature of::
287 307
288 308 callback(widget, **kwargs)
289 309
290 310 kwargs from display are passed through without modification.
291 311 remove: bool
292 312 True if the callback should be unregistered."""
293 313 self._display_callbacks.register_callback(callback, remove=remove)
294 314
295 315 def add_trait(self, traitname, trait):
296 316 """Dynamically add a trait attribute to the Widget."""
297 317 super(Widget, self).add_trait(traitname, trait)
298 318 if trait.get_metadata('sync'):
299 319 self.keys.append(traitname)
300 320 self.send_state(traitname)
301 321
302 322 #-------------------------------------------------------------------------
303 323 # Support methods
304 324 #-------------------------------------------------------------------------
305 325 @contextmanager
306 326 def _lock_property(self, key, value):
307 327 """Lock a property-value pair.
308 328
309 329 The value should be the JSON state of the property.
310 330
311 331 NOTE: This, in addition to the single lock for all state changes, is
312 332 flawed. In the future we may want to look into buffering state changes
313 333 back to the front-end."""
314 334 self._property_lock = (key, value)
315 335 try:
316 336 yield
317 337 finally:
318 338 self._property_lock = (None, None)
319 339
320 340 @contextmanager
321 341 def hold_sync(self):
322 342 """Hold syncing any state until the context manager is released"""
323 343 # We increment a value so that this can be nested. Syncing will happen when
324 344 # all levels have been released.
325 345 self._send_state_lock += 1
326 346 try:
327 347 yield
328 348 finally:
329 349 self._send_state_lock -=1
330 350 if self._send_state_lock == 0:
331 351 self.send_state(self._states_to_send)
332 352 self._states_to_send.clear()
333 353
334 354 def _should_send_property(self, key, value):
335 355 """Check the property lock (property_lock)"""
336 356 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
337 357 if (key == self._property_lock[0]
338 358 and to_json(value) == self._property_lock[1]):
339 359 return False
340 360 elif self._send_state_lock > 0:
341 361 self._states_to_send.add(key)
342 362 return False
343 363 else:
344 364 return True
345 365
346 366 # Event handlers
347 367 @_show_traceback
348 368 def _handle_msg(self, msg):
349 369 """Called when a msg is received from the front-end"""
350 370 data = msg['content']['data']
351 371 method = data['method']
352 372
353 373 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
354 374 if method == 'backbone':
355 375 if 'sync_data' in data:
376 # get binary buffers too
356 377 sync_data = data['sync_data']
378 for i,k in enumerate(data.get('buffer_keys', [])):
379 sync_data[k] = msg['buffers'][i]
357 380 self.set_state(sync_data) # handles all methods
358 381
359 382 # Handle a state request.
360 383 elif method == 'request_state':
361 384 self.send_state()
362 385
363 386 # Handle a custom msg from the front-end.
364 387 elif method == 'custom':
365 388 if 'content' in data:
366 self._handle_custom_msg(data['content'])
389 self._handle_custom_msg(data['content'], msg['buffers'])
367 390
368 391 # Catch remainder.
369 392 else:
370 393 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
371 394
372 def _handle_custom_msg(self, content):
395 def _handle_custom_msg(self, content, buffers):
373 396 """Called when a custom msg is received."""
374 self._msg_callbacks(self, content)
397 self._msg_callbacks(self, content, buffers)
375 398
376 399 def _notify_trait(self, name, old_value, new_value):
377 400 """Called when a property has been changed."""
378 401 # Trigger default traitlet callback machinery. This allows any user
379 402 # registered validation to be processed prior to allowing the widget
380 403 # machinery to handle the state.
381 404 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
382 405
383 406 # Send the state after the user registered callbacks for trait changes
384 407 # have all fired (allows for user to validate values).
385 408 if self.comm is not None and name in self.keys:
386 409 # Make sure this isn't information that the front-end just sent us.
387 410 if self._should_send_property(name, new_value):
388 411 # Send new state to front-end
389 412 self.send_state(key=name)
390 413
391 414 def _handle_displayed(self, **kwargs):
392 415 """Called when a view has been displayed for this widget instance"""
393 416 self._display_callbacks(self, **kwargs)
394 417
395 418 def _trait_to_json(self, x):
396 """Convert a trait value to json
397
398 Traverse lists/tuples and dicts and serialize their values as well.
399 Replace any widgets with their model_id
400 """
401 if isinstance(x, dict):
402 return {k: self._trait_to_json(v) for k, v in x.items()}
403 elif isinstance(x, (list, tuple)):
404 return [self._trait_to_json(v) for v in x]
405 elif isinstance(x, Widget):
406 return "IPY_MODEL_" + x.model_id
407 else:
408 return x # Value must be JSON-able
419 """Convert a trait value to json."""
420 return x
409 421
410 422 def _trait_from_json(self, x):
411 """Convert json values to objects
412
413 Replace any strings representing valid model id values to Widget references.
414 """
415 if isinstance(x, dict):
416 return {k: self._trait_from_json(v) for k, v in x.items()}
417 elif isinstance(x, (list, tuple)):
418 return [self._trait_from_json(v) for v in x]
419 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
420 # we want to support having child widgets at any level in a hierarchy
421 # trusting that a widget UUID will not appear out in the wild
422 return Widget.widgets[x[10:]]
423 else:
424 return x
423 """Convert json values to objects."""
424 return x
425 425
426 426 def _ipython_display_(self, **kwargs):
427 427 """Called when `IPython.display.display` is called on the widget."""
428 428 # Show view.
429 429 if self._view_name is not None:
430 430 self._send({"method": "display"})
431 431 self._handle_displayed(**kwargs)
432 432
433 def _send(self, msg):
433 def _send(self, msg, buffers=None):
434 434 """Sends a message to the model in the front-end."""
435 self.comm.send(msg)
435 self.comm.send(data=msg, buffers=buffers)
436 436
437 437
438 438 class DOMWidget(Widget):
439 439 visible = Bool(True, allow_none=True, help="Whether the widget is visible. False collapses the empty space, while None preserves the empty space.", sync=True)
440 440 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
441 441 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
442 442
443 443 width = CUnicode(sync=True)
444 444 height = CUnicode(sync=True)
445 445 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
446 446 padding = CUnicode(sync=True)
447 447 margin = CUnicode(sync=True)
448 448
449 449 color = Color(None, allow_none=True, sync=True)
450 450 background_color = Color(None, allow_none=True, sync=True)
451 451 border_color = Color(None, allow_none=True, sync=True)
452 452
453 453 border_width = CUnicode(sync=True)
454 454 border_radius = CUnicode(sync=True)
455 455 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
456 456 'none',
457 457 'hidden',
458 458 'dotted',
459 459 'dashed',
460 460 'solid',
461 461 'double',
462 462 'groove',
463 463 'ridge',
464 464 'inset',
465 465 'outset',
466 466 'initial',
467 467 'inherit', ''],
468 468 default_value='', sync=True)
469 469
470 470 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
471 471 'normal',
472 472 'italic',
473 473 'oblique',
474 474 'initial',
475 475 'inherit', ''],
476 476 default_value='', sync=True)
477 477 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
478 478 'normal',
479 479 'bold',
480 480 'bolder',
481 481 'lighter',
482 482 'initial',
483 483 'inherit', ''] + list(map(str, range(100,1000,100))),
484 484 default_value='', sync=True)
485 485 font_size = CUnicode(sync=True)
486 486 font_family = Unicode(sync=True)
487 487
488 488 def __init__(self, *pargs, **kwargs):
489 489 super(DOMWidget, self).__init__(*pargs, **kwargs)
490 490
491 491 def _validate_border(name, old, new):
492 492 if new is not None and new != '':
493 493 if name != 'border_width' and not self.border_width:
494 494 self.border_width = 1
495 495 if name != 'border_style' and self.border_style == '':
496 496 self.border_style = 'solid'
497 497 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,80 +1,107 b''
1 1 """Box class.
2 2
3 3 Represents a container that can be used to group other widgets.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 from .widget import DOMWidget, register
9 from .widget import DOMWidget, Widget, register
10 10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 11 from IPython.utils.warn import DeprecatedClass
12 12
13 def _widget_to_json(x):
14 if isinstance(x, dict):
15 return {k: _widget_to_json(v) for k, v in x.items()}
16 elif isinstance(x, (list, tuple)):
17 return [_widget_to_json(v) for v in x]
18 elif isinstance(x, Widget):
19 return "IPY_MODEL_" + x.model_id
20 else:
21 return x
22
23 def _json_to_widget(x):
24 if isinstance(x, dict):
25 return {k: _json_to_widget(v) for k, v in x.items()}
26 elif isinstance(x, (list, tuple)):
27 return [_json_to_widget(v) for v in x]
28 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
29 return Widget.widgets[x[10:]]
30 else:
31 return x
32
33 widget_serialization = {
34 'from_json': _json_to_widget,
35 'to_json': _widget_to_json
36 }
37
38
13 39 @register('IPython.Box')
14 40 class Box(DOMWidget):
15 41 """Displays multiple widgets in a group."""
42 _model_name = Unicode('BoxModel', sync=True)
16 43 _view_name = Unicode('BoxView', sync=True)
17 44
18 45 # Child widgets in the container.
19 46 # Using a tuple here to force reassignment to update the list.
20 47 # When a proper notifying-list trait exists, that is what should be used here.
21 children = Tuple(sync=True)
48 children = Tuple(sync=True, **widget_serialization)
22 49
23 50 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 51 overflow_x = CaselessStrEnum(
25 52 values=_overflow_values,
26 53 default_value='', sync=True, help="""Specifies what
27 54 happens to content that is too large for the rendered region.""")
28 55 overflow_y = CaselessStrEnum(
29 56 values=_overflow_values,
30 57 default_value='', sync=True, help="""Specifies what
31 58 happens to content that is too large for the rendered region.""")
32 59
33 60 box_style = CaselessStrEnum(
34 61 values=['success', 'info', 'warning', 'danger', ''],
35 62 default_value='', allow_none=True, sync=True, help="""Use a
36 63 predefined styling for the box.""")
37 64
38 65 def __init__(self, children = (), **kwargs):
39 66 kwargs['children'] = children
40 67 super(Box, self).__init__(**kwargs)
41 68 self.on_displayed(Box._fire_children_displayed)
42 69
43 70 def _fire_children_displayed(self):
44 71 for child in self.children:
45 72 child._handle_displayed()
46 73
47 74
48 75 @register('IPython.FlexBox')
49 76 class FlexBox(Box):
50 77 """Displays multiple widgets using the flexible box model."""
51 78 _view_name = Unicode('FlexBoxView', sync=True)
52 79 orientation = CaselessStrEnum(values=['vertical', 'horizontal'], default_value='vertical', sync=True)
53 80 flex = Int(0, sync=True, help="""Specify the flexible-ness of the model.""")
54 81 def _flex_changed(self, name, old, new):
55 82 new = min(max(0, new), 2)
56 83 if self.flex != new:
57 84 self.flex = new
58 85
59 86 _locations = ['start', 'center', 'end', 'baseline', 'stretch']
60 87 pack = CaselessStrEnum(
61 88 values=_locations,
62 89 default_value='start', sync=True)
63 90 align = CaselessStrEnum(
64 91 values=_locations,
65 92 default_value='start', sync=True)
66 93
67 94
68 95 def VBox(*pargs, **kwargs):
69 96 """Displays multiple widgets vertically using the flexible box model."""
70 97 kwargs['orientation'] = 'vertical'
71 98 return FlexBox(*pargs, **kwargs)
72 99
73 100 def HBox(*pargs, **kwargs):
74 101 """Displays multiple widgets horizontally using the flexible box model."""
75 102 kwargs['orientation'] = 'horizontal'
76 103 return FlexBox(*pargs, **kwargs)
77 104
78 105
79 106 # Remove in IPython 4.0
80 107 ContainerWidget = DeprecatedClass(Box, 'ContainerWidget')
@@ -1,82 +1,82 b''
1 1 """Button class.
2 2
3 3 Represents a button in the frontend using a widget. Allows user to listen for
4 4 click events on the button and trigger backend code when the clicks are fired.
5 5 """
6 6 #-----------------------------------------------------------------------------
7 7 # Copyright (c) 2013, the IPython Development Team.
8 8 #
9 9 # Distributed under the terms of the Modified BSD License.
10 10 #
11 11 # The full license is in the file COPYING.txt, distributed with this software.
12 12 #-----------------------------------------------------------------------------
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Imports
16 16 #-----------------------------------------------------------------------------
17 17 from .widget import DOMWidget, CallbackDispatcher, register
18 18 from IPython.utils.traitlets import Unicode, Bool, CaselessStrEnum
19 19 from IPython.utils.warn import DeprecatedClass
20 20
21 21 #-----------------------------------------------------------------------------
22 22 # Classes
23 23 #-----------------------------------------------------------------------------
24 24 @register('IPython.Button')
25 25 class Button(DOMWidget):
26 26 """Button widget.
27 27 This widget has an `on_click` method that allows you to listen for the
28 28 user clicking on the button. The click event itself is stateless.
29 29
30 30 Parameters
31 31 ----------
32 32 description : str
33 33 description displayed next to the button
34 34 tooltip: str
35 35 tooltip caption of the toggle button
36 36 icon: str
37 37 font-awesome icon name
38 38 """
39 39 _view_name = Unicode('ButtonView', sync=True)
40 40
41 41 # Keys
42 42 description = Unicode('', help="Button label.", sync=True)
43 43 tooltip = Unicode(help="Tooltip caption of the button.", sync=True)
44 44 disabled = Bool(False, help="Enable or disable user changes.", sync=True)
45 45 icon = Unicode('', help= "Font-awesome icon.", sync=True)
46 46
47 47 button_style = CaselessStrEnum(
48 48 values=['primary', 'success', 'info', 'warning', 'danger', ''],
49 49 default_value='', allow_none=True, sync=True, help="""Use a
50 50 predefined styling for the button.""")
51 51
52 52 def __init__(self, **kwargs):
53 53 """Constructor"""
54 54 super(Button, self).__init__(**kwargs)
55 55 self._click_handlers = CallbackDispatcher()
56 56 self.on_msg(self._handle_button_msg)
57 57
58 58 def on_click(self, callback, remove=False):
59 59 """Register a callback to execute when the button is clicked.
60 60
61 61 The callback will be called with one argument,
62 62 the clicked button widget instance.
63 63
64 64 Parameters
65 65 ----------
66 66 remove : bool (optional)
67 67 Set to true to remove the callback from the list of callbacks."""
68 68 self._click_handlers.register_callback(callback, remove=remove)
69 69
70 def _handle_button_msg(self, _, content):
70 def _handle_button_msg(self, _, content, buffers):
71 71 """Handle a msg from the front-end.
72 72
73 73 Parameters
74 74 ----------
75 75 content: dict
76 76 Content of the msg."""
77 77 if content.get('event', '') == 'click':
78 78 self._click_handlers(self)
79 79
80 80
81 81 # Remove in IPython 4.0
82 82 ButtonWidget = DeprecatedClass(Button, 'ButtonWidget')
General Comments 0
You need to be logged in to leave comments. Login now