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