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