##// END OF EJS Templates
Change custom serialization to use custom models, rather than transmitting the serializer name across the wire...
Jason Grout -
Show More
@@ -1,34 +1,49 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 6 "widgets/js/widget_link",
7 7 "widgets/js/widget_bool",
8 8 "widgets/js/widget_button",
9 9 "widgets/js/widget_box",
10 10 "widgets/js/widget_float",
11 11 "widgets/js/widget_image",
12 12 "widgets/js/widget_int",
13 13 "widgets/js/widget_output",
14 14 "widgets/js/widget_selection",
15 15 "widgets/js/widget_selectioncontainer",
16 16 "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]);
17 ], function(widgetmanager) {
18
19
20 /**
21 * From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
22 * Can be removed with the string endsWith function is implemented in major browsers
23 */
24 var endsWith = function(target, searchString, position) {
25 var subjectString = target.toString();
26 if (position === undefined || position > subjectString.length) {
27 position = subjectString.length;
21 28 }
22 }
29 position -= searchString.length;
30 var lastIndex = subjectString.indexOf(searchString, position);
31 return lastIndex !== -1 && lastIndex === position;
32 };
23 33
24 // Register all of the loaded views with the widget manager.
25 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]);
34 // Register all of the loaded models and views with the widget manager.
35 for (var i = 1; i < arguments.length; i++) {
36 var module = arguments[i];
37 for (var target_name in module) {
38 if (module.hasOwnProperty(target_name)) {
39 if (endsWith(target_name, "View")) {
40 widgetmanager.WidgetManager.register_widget_view(target_name, module[target_name]);
41 } else if (endsWith(target_name, "Model")) {
42 widgetmanager.WidgetManager.register_widget_model(target_name, module[target_name]);
43 }
29 44 }
30 45 }
31 46 }
32 47
33 48 return {'WidgetManager': widgetmanager.WidgetManager};
34 49 });
@@ -1,824 +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 this.serializers = {};
36 35 this._resolve_received_state = {};
37 36
38 37 if (comm !== undefined) {
39 38 // Remember comm associated with the model.
40 39 this.comm = comm;
41 40 comm.model = this;
42 41
43 42 // Hook comm messages up to model.
44 43 comm.on_close($.proxy(this._handle_comm_closed, this));
45 44 comm.on_msg($.proxy(this._handle_comm_msg, this));
46 45
47 46 // Assume the comm is alive.
48 47 this.set_comm_live(true);
49 48 } else {
50 49 this.set_comm_live(false);
51 50 }
52 51
53 52 // Listen for the events that lead to the websocket being terminated.
54 53 var that = this;
55 54 var died = function() {
56 55 that.set_comm_live(false);
57 56 };
58 57 widget_manager.notebook.events.on('kernel_disconnected.Kernel', died);
59 58 widget_manager.notebook.events.on('kernel_killed.Kernel', died);
60 59 widget_manager.notebook.events.on('kernel_restarting.Kernel', died);
61 60 widget_manager.notebook.events.on('kernel_dead.Kernel', died);
62 61
63 62 return Backbone.Model.apply(this);
64 63 },
65 64
66 65 send: function (content, callbacks, buffers) {
67 66 /**
68 67 * Send a custom msg over the comm.
69 68 */
70 69 if (this.comm !== undefined) {
71 70 var data = {method: 'custom', content: content};
72 71 this.comm.send(data, callbacks, {}, buffers);
73 72 this.pending_msgs++;
74 73 }
75 74 },
76 75
77 76 request_state: function(callbacks) {
78 77 /**
79 78 * Request a state push from the back-end.
80 79 */
81 80 if (!this.comm) {
82 81 console.error("Could not request_state because comm doesn't exist!");
83 82 return;
84 83 }
85 84
86 85 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
87 86
88 87 // Promise that is resolved when a state is received
89 88 // from the back-end.
90 89 var that = this;
91 90 var received_state = new Promise(function(resolve) {
92 91 that._resolve_received_state[msg_id] = resolve;
93 92 });
94 93 return received_state;
95 94 },
96 95
97 96 set_comm_live: function(live) {
98 97 /**
99 98 * Change the comm_live state of the model.
100 99 */
101 100 if (this.comm_live === undefined || this.comm_live != live) {
102 101 this.comm_live = live;
103 102 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
104 103 }
105 104 },
106 105
107 106 close: function(comm_closed) {
108 107 /**
109 108 * Close model
110 109 */
111 110 if (this.comm && !comm_closed) {
112 111 this.comm.close();
113 112 }
114 113 this.stopListening();
115 114 this.trigger('destroy', this);
116 115 delete this.comm.model; // Delete ref so GC will collect widget model.
117 116 delete this.comm;
118 117 delete this.model_id; // Delete id from model so widget manager cleans up.
119 118 _.each(this.views, function(v, id, views) {
120 119 v.then(function(view) {
121 120 view.remove();
122 121 delete views[id];
123 122 });
124 123 });
125 124 },
126 125
127 126 _handle_comm_closed: function (msg) {
128 127 /**
129 128 * Handle when a widget is closed.
130 129 */
131 130 this.trigger('comm:close');
132 131 this.close(true);
133 132 },
134 133
135 134 _handle_comm_msg: function (msg) {
136 135 /**
137 136 * Handle incoming comm msg.
138 137 */
139 138 var method = msg.content.data.method;
140 139
141 140 var that = this;
142 141 switch (method) {
143 142 case 'update':
144 143 this.state_change = this.state_change
145 144 .then(function() {
146 145 var state = msg.content.data.state || {};
147 146 var buffer_keys = msg.content.data.buffers || [];
148 147 var buffers = msg.buffers || [];
149 var metadata = msg.content.data.metadata || {};
150 var i,k;
151 148 for (var i=0; i<buffer_keys.length; i++) {
152 k = buffer_keys[i];
153 state[k] = buffers[i];
149 state[buffer_keys[i]] = buffers[i];
154 150 }
155 151
156 // for any metadata specifying a deserializer, set the
157 // state to a promise that resolves to the deserialized version
158 // also, store the serialization function for the attribute
159 var keys = Object.keys(metadata);
160 for (var i=0; i<keys.length; i++) {
161 k = keys[i];
162 if (metadata[k] && metadata[k].serialization) {
163 that.serializers[k] = utils.load_class.apply(that,
164 metadata[k].serialization);
165 state[k] = that.deserialize(that.serializers[k], state[k]);
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 }
166 159 }
167 160 }
168 161 return utils.resolve_promises_dict(state);
169 162 }).then(function(state) {
170 163 return that.set_state(state);
171 164 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
172 165 .then(function() {
173 166 var parent_id = msg.parent_header.msg_id;
174 167 if (that._resolve_received_state[parent_id] !== undefined) {
175 168 that._resolve_received_state[parent_id].call();
176 169 delete that._resolve_received_state[parent_id];
177 170 }
178 171 }).catch(utils.reject("Couldn't resolve state request promise", true));
179 172 break;
180 173 case 'custom':
181 174 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
182 175 break;
183 176 case 'display':
184 177 this.state_change = this.state_change.then(function() {
185 178 that.widget_manager.display_view(msg, that);
186 179 }).catch(utils.reject('Could not process display view msg', true));
187 180 break;
188 181 }
189 182 },
190 183
191 deserialize: function(serializer, value) {
192 // given a serializer dict and a value,
193 // return a promise for the deserialized value
194 var that = this;
195 return serializer.then(function(s) {
196 if (s.deserialize) {
197 return s.deserialize(value, that);
198 } else {
199 return value;
200 }
201 });
202 },
203
204 184 set_state: function (state) {
205 185 var that = this;
206 186 // Handle when a widget is updated via the python side.
207 187 return new Promise(function(resolve, reject) {
208 188 that.state_lock = state;
209 189 try {
210 190 WidgetModel.__super__.set.call(that, state);
211 191 } finally {
212 192 that.state_lock = null;
213 193 }
214 194 resolve();
215 195 }).catch(utils.reject("Couldn't set model state", true));
216 196 },
217 197
218 198 get_state: function() {
219 199 // Get the serializable state of the model.
220 200 // Equivalent to Backbone.Model.toJSON()
221 201 return _.clone(this.attributes);
222 202 },
223 203
224 204 _handle_status: function (msg, callbacks) {
225 205 /**
226 206 * Handle status msgs.
227 207 *
228 208 * execution_state : ('busy', 'idle', 'starting')
229 209 */
230 210 if (this.comm !== undefined) {
231 211 if (msg.content.execution_state ==='idle') {
232 212 // Send buffer if this message caused another message to be
233 213 // throttled.
234 214 if (this.msg_buffer !== null &&
235 215 (this.get('msg_throttle') || 3) === this.pending_msgs) {
236 216 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
237 217 this.comm.send(data, callbacks);
238 218 this.msg_buffer = null;
239 219 } else {
240 220 --this.pending_msgs;
241 221 }
242 222 }
243 223 }
244 224 },
245 225
246 226 callbacks: function(view) {
247 227 /**
248 228 * Create msg callbacks for a comm msg.
249 229 */
250 230 var callbacks = this.widget_manager.callbacks(view);
251 231
252 232 if (callbacks.iopub === undefined) {
253 233 callbacks.iopub = {};
254 234 }
255 235
256 236 var that = this;
257 237 callbacks.iopub.status = function (msg) {
258 238 that._handle_status(msg, callbacks);
259 239 };
260 240 return callbacks;
261 241 },
262 242
263 243 set: function(key, val, options) {
264 244 /**
265 245 * Set a value.
266 246 */
267 247 var return_value = WidgetModel.__super__.set.apply(this, arguments);
268 248
269 249 // Backbone only remembers the diff of the most recent set()
270 250 // operation. Calling set multiple times in a row results in a
271 251 // loss of diff information. Here we keep our own running diff.
272 252 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
273 253 return return_value;
274 254 },
275 255
276 256 sync: function (method, model, options) {
277 257 /**
278 258 * Handle sync to the back-end. Called when a model.save() is called.
279 259 *
280 260 * Make sure a comm exists.
281 261
282 262 * Parameters
283 263 * ----------
284 264 * method : create, update, patch, delete, read
285 265 * create/update always send the full attribute set
286 266 * patch - only send attributes listed in options.attrs, and if we are queuing
287 267 * up messages, combine with previous messages that have not been sent yet
288 268 * model : the model we are syncing
289 269 * will normally be the same as `this`
290 270 * options : dict
291 271 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
292 272 * otherwise, sync all attributes
293 273 *
294 274 */
295 275 var error = options.error || function() {
296 276 console.error('Backbone sync error:', arguments);
297 277 };
298 278 if (this.comm === undefined) {
299 279 error();
300 280 return false;
301 281 }
302 282
303 283 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
304 284
305 285 // the state_lock lists attributes that are currently be changed right now from a kernel message
306 286 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
307 287 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
308 288 if (this.state_lock !== null) {
309 289 var keys = Object.keys(this.state_lock);
310 290 for (var i=0; i<keys.length; i++) {
311 291 var key = keys[i];
312 292 if (attrs[key] === this.state_lock[key]) {
313 293 delete attrs[key];
314 294 }
315 295 }
316 296 }
317 297
318 298 if (_.size(attrs) > 0) {
319 299
320 300 // If this message was sent via backbone itself, it will not
321 301 // have any callbacks. It's important that we create callbacks
322 302 // so we can listen for status messages, etc...
323 303 var callbacks = options.callbacks || this.callbacks();
324 304
325 305 // Check throttle.
326 306 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
327 307 // The throttle has been exceeded, buffer the current msg so
328 308 // it can be sent once the kernel has finished processing
329 309 // some of the existing messages.
330 310
331 311 // Combine updates if it is a 'patch' sync, otherwise replace updates
332 312 switch (method) {
333 313 case 'patch':
334 314 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
335 315 break;
336 316 case 'update':
337 317 case 'create':
338 318 this.msg_buffer = attrs;
339 319 break;
340 320 default:
341 321 error();
342 322 return false;
343 323 }
344 324 this.msg_buffer_callbacks = callbacks;
345 325
346 326 } else {
347 327 // We haven't exceeded the throttle, send the message like
348 328 // normal.
349 329 this.send_sync_message(attrs, callbacks);
350 330 this.pending_msgs++;
351 331 }
352 332 }
353 333 // Since the comm is a one-way communication, assume the message
354 334 // arrived. Don't call success since we don't have a model back from the server
355 335 // this means we miss out on the 'sync' event.
356 336 this._buffered_state_diff = {};
357 337 },
358 338
359 339
360 340 send_sync_message: function(attrs, callbacks) {
361 341 // prepare and send a comm message syncing attrs
362 342 var that = this;
363 343 // first, build a state dictionary with key=the attribute and the value
364 344 // being the value or the promise of the serialized value
365 var state_promise_dict = {};
366 var keys = Object.keys(attrs);
367 for (var i=0; i<keys.length; i++) {
368 // bind k and v locally; needed since we have an inner async function using v
369 (function(k,v) {
370 if (that.serializers[k]) {
371 state_promise_dict[k] = that.serializers[k].then(function(f) {
372 if (f.serialize) {
373 return f.serialize(v, that);
374 } else {
375 return v;
376 }
377 })
378 } else {
379 state_promise_dict[k] = v;
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);
380 350 }
381 })(keys[i], attrs[keys[i]])
351 }
382 352 }
383 utils.resolve_promises_dict(state_promise_dict).then(function(state) {
353 utils.resolve_promises_dict(attrs).then(function(state) {
384 354 // get binary values, then send
385 355 var keys = Object.keys(state);
386 356 var buffers = [];
387 357 var buffer_keys = [];
388 358 for (var i=0; i<keys.length; i++) {
389 359 var key = keys[i];
390 360 var value = state[key];
391 361 if (value.buffer instanceof ArrayBuffer
392 362 || value instanceof ArrayBuffer) {
393 363 buffers.push(value);
394 364 buffer_keys.push(key);
395 365 delete state[key];
396 366 }
397 367 }
398 368 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
399 369 }).catch(function(error) {
400 370 that.pending_msgs--;
401 371 return (utils.reject("Couldn't send widget sync message", true))(error);
402 372 });
403 373 },
404 374
405 serialize: function(model, attrs) {
406 // Serialize the attributes into a sync message
407 var keys = Object.keys(attrs);
408 var key, value;
409 var buffers, metadata, buffer_keys, serialize;
410 for (var i=0; i<keys.length; i++) {
411 key = keys[i];
412 serialize = model.serializers[key];
413 if (serialize && serialize.serialize) {
414 attrs[key] = serialize.serialize(attrs[key]);
415 }
416 }
417 },
418
419 375 save_changes: function(callbacks) {
420 376 /**
421 377 * Push this model's state to the back-end
422 378 *
423 379 * This invokes a Backbone.Sync.
424 380 */
425 381 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
426 382 },
427 383
428 384 on_some_change: function(keys, callback, context) {
429 385 /**
430 386 * on_some_change(["key1", "key2"], foo, context) differs from
431 387 * on("change:key1 change:key2", foo, context).
432 388 * If the widget attributes key1 and key2 are both modified,
433 389 * the second form will result in foo being called twice
434 390 * while the first will call foo only once.
435 391 */
436 392 this.on('change', function() {
437 393 if (keys.some(this.hasChanged, this)) {
438 394 callback.apply(context);
439 395 }
440 396 }, this);
441 397
442 398 },
443 399
444 400 toJSON: function(options) {
445 401 /**
446 402 * Serialize the model. See the types.js deserialization function
447 403 * and the kernel-side serializer/deserializer
448 404 */
449 405 return "IPY_MODEL_"+this.id;
450 406 }
451 407 });
452 408 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
453 409
454 410
455 411 var WidgetView = Backbone.View.extend({
456 412 initialize: function(parameters) {
457 413 /**
458 414 * Public constructor.
459 415 */
460 416 this.model.on('change',this.update,this);
461 417
462 418 // Bubble the comm live events.
463 419 this.model.on('comm:live', function() {
464 420 this.trigger('comm:live', this);
465 421 }, this);
466 422 this.model.on('comm:dead', function() {
467 423 this.trigger('comm:dead', this);
468 424 }, this);
469 425
470 426 this.options = parameters.options;
471 427 this.on('displayed', function() {
472 428 this.is_displayed = true;
473 429 }, this);
474 430 },
475 431
476 432 update: function(){
477 433 /**
478 434 * Triggered on model change.
479 435 *
480 436 * Update view to be consistent with this.model
481 437 */
482 438 },
483 439
484 440 create_child_view: function(child_model, options) {
485 441 /**
486 442 * Create and promise that resolves to a child view of a given model
487 443 */
488 444 var that = this;
489 445 options = $.extend({ parent: this }, options || {});
490 446 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true));
491 447 },
492 448
493 449 callbacks: function(){
494 450 /**
495 451 * Create msg callbacks for a comm msg.
496 452 */
497 453 return this.model.callbacks(this);
498 454 },
499 455
500 456 render: function(){
501 457 /**
502 458 * Render the view.
503 459 *
504 460 * By default, this is only called the first time the view is created
505 461 */
506 462 },
507 463
508 464 send: function (content, buffers) {
509 465 /**
510 466 * Send a custom msg associated with this view.
511 467 */
512 468 this.model.send(content, this.callbacks(), buffers);
513 469 },
514 470
515 471 touch: function () {
516 472 this.model.save_changes(this.callbacks());
517 473 },
518 474
519 475 after_displayed: function (callback, context) {
520 476 /**
521 477 * Calls the callback right away is the view is already displayed
522 478 * otherwise, register the callback to the 'displayed' event.
523 479 */
524 480 if (this.is_displayed) {
525 481 callback.apply(context);
526 482 } else {
527 483 this.on('displayed', callback, context);
528 484 }
529 485 },
530 486
531 487 remove: function () {
532 488 // Raise a remove event when the view is removed.
533 489 WidgetView.__super__.remove.apply(this, arguments);
534 490 this.trigger('remove');
535 491 }
536 492 });
537 493
538 494
539 495 var DOMWidgetView = WidgetView.extend({
540 496 initialize: function (parameters) {
541 497 /**
542 498 * Public constructor
543 499 */
544 500 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
545 501 this.model.on('change:visible', this.update_visible, this);
546 502 this.model.on('change:_css', this.update_css, this);
547 503
548 504 this.model.on('change:_dom_classes', function(model, new_classes) {
549 505 var old_classes = model.previous('_dom_classes');
550 506 this.update_classes(old_classes, new_classes);
551 507 }, this);
552 508
553 509 this.model.on('change:color', function (model, value) {
554 510 this.update_attr('color', value); }, this);
555 511
556 512 this.model.on('change:background_color', function (model, value) {
557 513 this.update_attr('background', value); }, this);
558 514
559 515 this.model.on('change:width', function (model, value) {
560 516 this.update_attr('width', value); }, this);
561 517
562 518 this.model.on('change:height', function (model, value) {
563 519 this.update_attr('height', value); }, this);
564 520
565 521 this.model.on('change:border_color', function (model, value) {
566 522 this.update_attr('border-color', value); }, this);
567 523
568 524 this.model.on('change:border_width', function (model, value) {
569 525 this.update_attr('border-width', value); }, this);
570 526
571 527 this.model.on('change:border_style', function (model, value) {
572 528 this.update_attr('border-style', value); }, this);
573 529
574 530 this.model.on('change:font_style', function (model, value) {
575 531 this.update_attr('font-style', value); }, this);
576 532
577 533 this.model.on('change:font_weight', function (model, value) {
578 534 this.update_attr('font-weight', value); }, this);
579 535
580 536 this.model.on('change:font_size', function (model, value) {
581 537 this.update_attr('font-size', this._default_px(value)); }, this);
582 538
583 539 this.model.on('change:font_family', function (model, value) {
584 540 this.update_attr('font-family', value); }, this);
585 541
586 542 this.model.on('change:padding', function (model, value) {
587 543 this.update_attr('padding', value); }, this);
588 544
589 545 this.model.on('change:margin', function (model, value) {
590 546 this.update_attr('margin', this._default_px(value)); }, this);
591 547
592 548 this.model.on('change:border_radius', function (model, value) {
593 549 this.update_attr('border-radius', this._default_px(value)); }, this);
594 550
595 551 this.after_displayed(function() {
596 552 this.update_visible(this.model, this.model.get("visible"));
597 553 this.update_classes([], this.model.get('_dom_classes'));
598 554
599 555 this.update_attr('color', this.model.get('color'));
600 556 this.update_attr('background', this.model.get('background_color'));
601 557 this.update_attr('width', this.model.get('width'));
602 558 this.update_attr('height', this.model.get('height'));
603 559 this.update_attr('border-color', this.model.get('border_color'));
604 560 this.update_attr('border-width', this.model.get('border_width'));
605 561 this.update_attr('border-style', this.model.get('border_style'));
606 562 this.update_attr('font-style', this.model.get('font_style'));
607 563 this.update_attr('font-weight', this.model.get('font_weight'));
608 564 this.update_attr('font-size', this._default_px(this.model.get('font_size')));
609 565 this.update_attr('font-family', this.model.get('font_family'));
610 566 this.update_attr('padding', this.model.get('padding'));
611 567 this.update_attr('margin', this._default_px(this.model.get('margin')));
612 568 this.update_attr('border-radius', this._default_px(this.model.get('border_radius')));
613 569
614 570 this.update_css(this.model, this.model.get("_css"));
615 571 }, this);
616 572 },
617 573
618 574 _default_px: function(value) {
619 575 /**
620 576 * Makes browser interpret a numerical string as a pixel value.
621 577 */
622 578 if (value && /^\d+\.?(\d+)?$/.test(value.trim())) {
623 579 return value.trim() + 'px';
624 580 }
625 581 return value;
626 582 },
627 583
628 584 update_attr: function(name, value) {
629 585 /**
630 586 * Set a css attr of the widget view.
631 587 */
632 588 this.$el.css(name, value);
633 589 },
634 590
635 591 update_visible: function(model, value) {
636 592 /**
637 593 * Update visibility
638 594 */
639 595 switch(value) {
640 596 case null: // python None
641 597 this.$el.show().css('visibility', 'hidden'); break;
642 598 case false:
643 599 this.$el.hide(); break;
644 600 case true:
645 601 this.$el.show().css('visibility', ''); break;
646 602 }
647 603 },
648 604
649 605 update_css: function (model, css) {
650 606 /**
651 607 * Update the css styling of this view.
652 608 */
653 609 if (css === undefined) {return;}
654 610 for (var i = 0; i < css.length; i++) {
655 611 // Apply the css traits to all elements that match the selector.
656 612 var selector = css[i][0];
657 613 var elements = this._get_selector_element(selector);
658 614 if (elements.length > 0) {
659 615 var trait_key = css[i][1];
660 616 var trait_value = css[i][2];
661 617 elements.css(trait_key ,trait_value);
662 618 }
663 619 }
664 620 },
665 621
666 622 update_classes: function (old_classes, new_classes, $el) {
667 623 /**
668 624 * Update the DOM classes applied to an element, default to this.$el.
669 625 */
670 626 if ($el===undefined) {
671 627 $el = this.$el;
672 628 }
673 629 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
674 630 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
675 631 },
676 632
677 633 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
678 634 /**
679 635 * Update the DOM classes applied to the widget based on a single
680 636 * trait's value.
681 637 *
682 638 * Given a trait value classes map, this function automatically
683 639 * handles applying the appropriate classes to the widget element
684 640 * and removing classes that are no longer valid.
685 641 *
686 642 * Parameters
687 643 * ----------
688 644 * class_map: dictionary
689 645 * Dictionary of trait values to class lists.
690 646 * Example:
691 647 * {
692 648 * success: ['alert', 'alert-success'],
693 649 * info: ['alert', 'alert-info'],
694 650 * warning: ['alert', 'alert-warning'],
695 651 * danger: ['alert', 'alert-danger']
696 652 * };
697 653 * trait_name: string
698 654 * Name of the trait to check the value of.
699 655 * previous_trait_value: optional string, default ''
700 656 * Last trait value
701 657 * $el: optional jQuery element handle, defaults to this.$el
702 658 * Element that the classes are applied to.
703 659 */
704 660 var key = previous_trait_value;
705 661 if (key === undefined) {
706 662 key = this.model.previous(trait_name);
707 663 }
708 664 var old_classes = class_map[key] ? class_map[key] : [];
709 665 key = this.model.get(trait_name);
710 666 var new_classes = class_map[key] ? class_map[key] : [];
711 667
712 668 this.update_classes(old_classes, new_classes, $el || this.$el);
713 669 },
714 670
715 671 _get_selector_element: function (selector) {
716 672 /**
717 673 * Get the elements via the css selector.
718 674 */
719 675 var elements;
720 676 if (!selector) {
721 677 elements = this.$el;
722 678 } else {
723 679 elements = this.$el.find(selector).addBack(selector);
724 680 }
725 681 return elements;
726 682 },
727 683
728 684 typeset: function(element, text){
729 685 utils.typeset.apply(null, arguments);
730 686 },
731 687 });
732 688
733 689
734 690 var ViewList = function(create_view, remove_view, context) {
735 691 /**
736 692 * - create_view and remove_view are default functions called when adding or removing views
737 693 * - create_view takes a model and returns a view or a promise for a view for that model
738 694 * - remove_view takes a view and destroys it (including calling `view.remove()`)
739 695 * - each time the update() function is called with a new list, the create and remove
740 696 * callbacks will be called in an order so that if you append the views created in the
741 697 * create callback and remove the views in the remove callback, you will duplicate
742 698 * the order of the list.
743 699 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
744 700 * - the context defaults to the created ViewList. If you pass another context, the create and remove
745 701 * will be called in that context.
746 702 */
747 703
748 704 this.initialize.apply(this, arguments);
749 705 };
750 706
751 707 _.extend(ViewList.prototype, {
752 708 initialize: function(create_view, remove_view, context) {
753 709 this._handler_context = context || this;
754 710 this._models = [];
755 711 this.views = []; // list of promises for views
756 712 this._create_view = create_view;
757 713 this._remove_view = remove_view || function(view) {view.remove();};
758 714 },
759 715
760 716 update: function(new_models, create_view, remove_view, context) {
761 717 /**
762 718 * the create_view, remove_view, and context arguments override the defaults
763 719 * specified when the list is created.
764 720 * after this function, the .views attribute is a list of promises for views
765 721 * if you want to perform some action on the list of views, do something like
766 722 * `Promise.all(myviewlist.views).then(function(views) {...});`
767 723 */
768 724 var remove = remove_view || this._remove_view;
769 725 var create = create_view || this._create_view;
770 726 context = context || this._handler_context;
771 727 var i = 0;
772 728 // first, skip past the beginning of the lists if they are identical
773 729 for (; i < new_models.length; i++) {
774 730 if (i >= this._models.length || new_models[i] !== this._models[i]) {
775 731 break;
776 732 }
777 733 }
778 734
779 735 var first_removed = i;
780 736 // Remove the non-matching items from the old list.
781 737 var removed = this.views.splice(first_removed, this.views.length-first_removed);
782 738 for (var j = 0; j < removed.length; j++) {
783 739 removed[j].then(function(view) {
784 740 remove.call(context, view)
785 741 });
786 742 }
787 743
788 744 // Add the rest of the new list items.
789 745 for (; i < new_models.length; i++) {
790 746 this.views.push(Promise.resolve(create.call(context, new_models[i])));
791 747 }
792 748 // make a copy of the input array
793 749 this._models = new_models.slice();
794 750 },
795 751
796 752 remove: function() {
797 753 /**
798 754 * removes every view in the list; convenience function for `.update([])`
799 755 * that should be faster
800 756 * returns a promise that resolves after this removal is done
801 757 */
802 758 var that = this;
803 759 return Promise.all(this.views).then(function(views) {
804 760 for (var i = 0; i < that.views.length; i++) {
805 761 that._remove_view.call(that._handler_context, views[i]);
806 762 }
807 763 that.views = [];
808 764 that._models = [];
809 765 });
810 766 },
811 767 });
812 768
813 769 var widget = {
814 770 'WidgetModel': WidgetModel,
815 771 'WidgetView': WidgetView,
816 772 'DOMWidgetView': DOMWidgetView,
817 773 'ViewList': ViewList,
818 774 };
819 775
820 776 // For backwards compatability.
821 777 $.extend(IPython, widget);
822 778
823 779 return widget;
824 780 });
@@ -1,154 +1,186 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 'BoxModel': BoxModel,
151 183 'BoxView': BoxView,
152 184 'FlexBoxView': FlexBoxView,
153 185 };
154 186 });
@@ -1,523 +1,490 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 def _widget_to_json(x):
93 if isinstance(x, dict):
94 return {k: _widget_to_json(v) for k, v in x.items()}
95 elif isinstance(x, (list, tuple)):
96 return [_widget_to_json(v) for v in x]
97 elif isinstance(x, Widget):
98 return "IPY_MODEL_" + x.model_id
99 else:
100 return x
101
102 def _json_to_widget(x):
103 if isinstance(x, dict):
104 return {k: _json_to_widget(v) for k, v in x.items()}
105 elif isinstance(x, (list, tuple)):
106 return [_json_to_widget(v) for v in x]
107 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
108 return Widget.widgets[x[10:]]
109 else:
110 return x
111
112 widget_serialization = {
113 'from_json': _json_to_widget,
114 'to_json': lambda x: (_widget_to_json(x), {'serialization': ('models', 'widgets/js/types')})
115 }
116
117 92 class Widget(LoggingConfigurable):
118 93 #-------------------------------------------------------------------------
119 94 # Class attributes
120 95 #-------------------------------------------------------------------------
121 96 _widget_construction_callback = None
122 97 widgets = {}
123 98 widget_types = {}
124 99
125 100 @staticmethod
126 101 def on_widget_constructed(callback):
127 102 """Registers a callback to be called when a widget is constructed.
128 103
129 104 The callback must have the following signature:
130 105 callback(widget)"""
131 106 Widget._widget_construction_callback = callback
132 107
133 108 @staticmethod
134 109 def _call_widget_constructed(widget):
135 110 """Static method, called when a widget is constructed."""
136 111 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
137 112 Widget._widget_construction_callback(widget)
138 113
139 114 @staticmethod
140 115 def handle_comm_opened(comm, msg):
141 116 """Static method, called when a widget is constructed."""
142 117 widget_class = import_item(msg['content']['data']['widget_class'])
143 118 widget = widget_class(comm=comm)
144 119
145 120
146 121 #-------------------------------------------------------------------------
147 122 # Traits
148 123 #-------------------------------------------------------------------------
149 124 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
150 125 in which to find _model_name. If empty, look in the global registry.""")
151 126 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
152 127 registered in the front-end to create and sync this widget with.""")
153 128 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
154 129 If empty, look in the global registry.""", sync=True)
155 130 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
156 131 to use to represent the widget.""", sync=True)
157 132 comm = Instance('IPython.kernel.comm.Comm')
158 133
159 134 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
160 135 front-end can send before receiving an idle msg from the back-end.""")
161 136
162 137 version = Int(0, sync=True, help="""Widget's version""")
163 138 keys = List()
164 139 def _keys_default(self):
165 140 return [name for name in self.traits(sync=True)]
166 141
167 142 _property_lock = Tuple((None, None))
168 143 _send_state_lock = Int(0)
169 144 _states_to_send = Set()
170 145 _display_callbacks = Instance(CallbackDispatcher, ())
171 146 _msg_callbacks = Instance(CallbackDispatcher, ())
172 147
173 148 #-------------------------------------------------------------------------
174 149 # (Con/de)structor
175 150 #-------------------------------------------------------------------------
176 151 def __init__(self, **kwargs):
177 152 """Public constructor"""
178 153 self._model_id = kwargs.pop('model_id', None)
179 154 super(Widget, self).__init__(**kwargs)
180 155
181 156 Widget._call_widget_constructed(self)
182 157 self.open()
183 158
184 159 def __del__(self):
185 160 """Object disposal"""
186 161 self.close()
187 162
188 163 #-------------------------------------------------------------------------
189 164 # Properties
190 165 #-------------------------------------------------------------------------
191 166
192 167 def open(self):
193 168 """Open a comm to the frontend if one isn't already open."""
194 169 if self.comm is None:
195 170 args = dict(target_name='ipython.widget',
196 171 data={'model_name': self._model_name,
197 172 'model_module': self._model_module})
198 173 if self._model_id is not None:
199 174 args['comm_id'] = self._model_id
200 175 self.comm = Comm(**args)
201 176
202 177 def _comm_changed(self, name, new):
203 178 """Called when the comm is changed."""
204 179 if new is None:
205 180 return
206 181 self._model_id = self.model_id
207 182
208 183 self.comm.on_msg(self._handle_msg)
209 184 Widget.widgets[self.model_id] = self
210 185
211 186 # first update
212 187 self.send_state()
213 188
214 189 @property
215 190 def model_id(self):
216 191 """Gets the model id of this widget.
217 192
218 193 If a Comm doesn't exist yet, a Comm will be created automagically."""
219 194 return self.comm.comm_id
220 195
221 196 #-------------------------------------------------------------------------
222 197 # Methods
223 198 #-------------------------------------------------------------------------
224 199
225 200 def close(self):
226 201 """Close method.
227 202
228 203 Closes the underlying comm.
229 204 When the comm is closed, all of the widget views are automatically
230 205 removed from the front-end."""
231 206 if self.comm is not None:
232 207 Widget.widgets.pop(self.model_id, None)
233 208 self.comm.close()
234 209 self.comm = None
235 210
236 211 def send_state(self, key=None):
237 212 """Sends the widget state, or a piece of it, to the front-end.
238 213
239 214 Parameters
240 215 ----------
241 216 key : unicode, or iterable (optional)
242 217 A single property's name or iterable of property names to sync with the front-end.
243 218 """
244 state, buffer_keys, buffers, metadata = self.get_state(key=key)
219 state, buffer_keys, buffers = self.get_state(key=key)
245 220 msg = {"method": "update", "state": state}
246 221 if buffer_keys:
247 222 msg['buffers'] = buffer_keys
248 if metadata:
249 msg['metadata'] = metadata
250 223 self._send(msg, buffers=buffers)
251 224
252 225 def get_state(self, key=None):
253 226 """Gets the widget state, or a piece of it.
254 227
255 228 Parameters
256 229 ----------
257 230 key : unicode or iterable (optional)
258 231 A single property's name or iterable of property names to get.
259 232
260 233 Returns
261 234 -------
262 235 state : dict of states
263 236 buffer_keys : list of strings
264 237 the values that are stored in buffers
265 238 buffers : list of binary memoryviews
266 239 values to transmit in binary
267 240 metadata : dict
268 241 metadata for each field: {key: metadata}
269 242 """
270 243 if key is None:
271 244 keys = self.keys
272 245 elif isinstance(key, string_types):
273 246 keys = [key]
274 247 elif isinstance(key, collections.Iterable):
275 248 keys = key
276 249 else:
277 250 raise ValueError("key must be a string, an iterable of keys, or None")
278 251 state = {}
279 252 buffers = []
280 253 buffer_keys = []
281 metadata = {}
282 254 for k in keys:
283 255 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
284 256 value = getattr(self, k)
285 serialized, md = f(value)
257 serialized = f(value)
286 258 if isinstance(serialized, memoryview):
287 259 buffers.append(serialized)
288 260 buffer_keys.append(k)
289 261 else:
290 262 state[k] = serialized
291 if md is not None:
292 metadata[k] = md
293 return state, buffer_keys, buffers, metadata
263 return state, buffer_keys, buffers
294 264
295 265 def set_state(self, sync_data):
296 266 """Called when a state is received from the front-end."""
297 267 for name in self.keys:
298 268 if name in sync_data:
299 269 json_value = sync_data[name]
300 270 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
301 271 with self._lock_property(name, json_value):
302 272 setattr(self, name, from_json(json_value))
303 273
304 274 def send(self, content, buffers=None):
305 275 """Sends a custom msg to the widget model in the front-end.
306 276
307 277 Parameters
308 278 ----------
309 279 content : dict
310 280 Content of the message to send.
311 281 buffers : list of binary buffers
312 282 Binary buffers to send with message
313 283 """
314 284 self._send({"method": "custom", "content": content}, buffers=buffers)
315 285
316 286 def on_msg(self, callback, remove=False):
317 287 """(Un)Register a custom msg receive callback.
318 288
319 289 Parameters
320 290 ----------
321 291 callback: callable
322 292 callback will be passed three arguments when a message arrives::
323 293
324 294 callback(widget, content, buffers)
325 295
326 296 remove: bool
327 297 True if the callback should be unregistered."""
328 298 self._msg_callbacks.register_callback(callback, remove=remove)
329 299
330 300 def on_displayed(self, callback, remove=False):
331 301 """(Un)Register a widget displayed callback.
332 302
333 303 Parameters
334 304 ----------
335 305 callback: method handler
336 306 Must have a signature of::
337 307
338 308 callback(widget, **kwargs)
339 309
340 310 kwargs from display are passed through without modification.
341 311 remove: bool
342 312 True if the callback should be unregistered."""
343 313 self._display_callbacks.register_callback(callback, remove=remove)
344 314
345 315 #-------------------------------------------------------------------------
346 316 # Support methods
347 317 #-------------------------------------------------------------------------
348 318 @contextmanager
349 319 def _lock_property(self, key, value):
350 320 """Lock a property-value pair.
351 321
352 322 The value should be the JSON state of the property.
353 323
354 324 NOTE: This, in addition to the single lock for all state changes, is
355 325 flawed. In the future we may want to look into buffering state changes
356 326 back to the front-end."""
357 327 self._property_lock = (key, value)
358 328 try:
359 329 yield
360 330 finally:
361 331 self._property_lock = (None, None)
362 332
363 333 @contextmanager
364 334 def hold_sync(self):
365 335 """Hold syncing any state until the context manager is released"""
366 336 # We increment a value so that this can be nested. Syncing will happen when
367 337 # all levels have been released.
368 338 self._send_state_lock += 1
369 339 try:
370 340 yield
371 341 finally:
372 342 self._send_state_lock -=1
373 343 if self._send_state_lock == 0:
374 344 self.send_state(self._states_to_send)
375 345 self._states_to_send.clear()
376 346
377 347 def _should_send_property(self, key, value):
378 348 """Check the property lock (property_lock)"""
379 349 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
380 350 if (key == self._property_lock[0]
381 351 and to_json(value) == self._property_lock[1]):
382 352 return False
383 353 elif self._send_state_lock > 0:
384 354 self._states_to_send.add(key)
385 355 return False
386 356 else:
387 357 return True
388 358
389 359 # Event handlers
390 360 @_show_traceback
391 361 def _handle_msg(self, msg):
392 362 """Called when a msg is received from the front-end"""
393 363 data = msg['content']['data']
394 364 method = data['method']
395 365
396 366 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
397 367 if method == 'backbone':
398 368 if 'sync_data' in data:
399 369 # get binary buffers too
400 370 sync_data = data['sync_data']
401 371 for i,k in enumerate(data.get('buffer_keys', [])):
402 372 sync_data[k] = msg['buffers'][i]
403 373 self.set_state(sync_data) # handles all methods
404 374
405 375 # Handle a state request.
406 376 elif method == 'request_state':
407 377 self.send_state()
408 378
409 379 # Handle a custom msg from the front-end.
410 380 elif method == 'custom':
411 381 if 'content' in data:
412 382 self._handle_custom_msg(data['content'], msg['buffers'])
413 383
414 384 # Catch remainder.
415 385 else:
416 386 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
417 387
418 388 def _handle_custom_msg(self, content, buffers):
419 389 """Called when a custom msg is received."""
420 390 self._msg_callbacks(self, content, buffers)
421 391
422 392 def _notify_trait(self, name, old_value, new_value):
423 393 """Called when a property has been changed."""
424 394 # Trigger default traitlet callback machinery. This allows any user
425 395 # registered validation to be processed prior to allowing the widget
426 396 # machinery to handle the state.
427 397 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
428 398
429 399 # Send the state after the user registered callbacks for trait changes
430 400 # have all fired (allows for user to validate values).
431 401 if self.comm is not None and name in self.keys:
432 402 # Make sure this isn't information that the front-end just sent us.
433 403 if self._should_send_property(name, new_value):
434 404 # Send new state to front-end
435 405 self.send_state(key=name)
436 406
437 407 def _handle_displayed(self, **kwargs):
438 408 """Called when a view has been displayed for this widget instance"""
439 409 self._display_callbacks(self, **kwargs)
440 410
441 411 def _trait_to_json(self, x):
442 """Convert a trait value to json.
443
444 Metadata (the second return value) is not sent
445 """
446 return x, None
412 """Convert a trait value to json."""
413 return x
447 414
448 415 def _trait_from_json(self, x):
449 416 """Convert json values to objects."""
450 417 return x
451 418
452 419 def _ipython_display_(self, **kwargs):
453 420 """Called when `IPython.display.display` is called on the widget."""
454 421 # Show view.
455 422 if self._view_name is not None:
456 423 self._send({"method": "display"})
457 424 self._handle_displayed(**kwargs)
458 425
459 426 def _send(self, msg, buffers=None):
460 427 """Sends a message to the model in the front-end."""
461 428 self.comm.send(data=msg, buffers=buffers)
462 429
463 430
464 431 class DOMWidget(Widget):
465 432 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)
466 433 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
467 434 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
468 435
469 436 width = CUnicode(sync=True)
470 437 height = CUnicode(sync=True)
471 438 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
472 439 padding = CUnicode(sync=True)
473 440 margin = CUnicode(sync=True)
474 441
475 442 color = Color(None, allow_none=True, sync=True)
476 443 background_color = Color(None, allow_none=True, sync=True)
477 444 border_color = Color(None, allow_none=True, sync=True)
478 445
479 446 border_width = CUnicode(sync=True)
480 447 border_radius = CUnicode(sync=True)
481 448 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
482 449 'none',
483 450 'hidden',
484 451 'dotted',
485 452 'dashed',
486 453 'solid',
487 454 'double',
488 455 'groove',
489 456 'ridge',
490 457 'inset',
491 458 'outset',
492 459 'initial',
493 460 'inherit', ''],
494 461 default_value='', sync=True)
495 462
496 463 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
497 464 'normal',
498 465 'italic',
499 466 'oblique',
500 467 'initial',
501 468 'inherit', ''],
502 469 default_value='', sync=True)
503 470 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
504 471 'normal',
505 472 'bold',
506 473 'bolder',
507 474 'lighter',
508 475 'initial',
509 476 'inherit', ''] + list(map(str, range(100,1000,100))),
510 477 default_value='', sync=True)
511 478 font_size = CUnicode(sync=True)
512 479 font_family = Unicode(sync=True)
513 480
514 481 def __init__(self, *pargs, **kwargs):
515 482 super(DOMWidget, self).__init__(*pargs, **kwargs)
516 483
517 484 def _validate_border(name, old, new):
518 485 if new is not None and new != '':
519 486 if name != 'border_width' and not self.border_width:
520 487 self.border_width = 1
521 488 if name != 'border_style' and self.border_style == '':
522 489 self.border_style = 'solid'
523 490 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, widget_serialization
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 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 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now