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