##// END OF EJS Templates
Address @minrk 's review comments.
Jonathan Frederic -
Show More
@@ -1,621 +1,621 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define(["widgets/js/manager",
4 define(["widgets/js/manager",
5 "underscore",
5 "underscore",
6 "backbone",
6 "backbone",
7 "jquery",
7 "jquery",
8 "base/js/namespace",
8 "base/js/namespace",
9 ], function(widgetmanager, _, Backbone, $, IPython){
9 ], function(widgetmanager, _, Backbone, $, IPython){
10
10
11 var WidgetModel = Backbone.Model.extend({
11 var WidgetModel = Backbone.Model.extend({
12 constructor: function (widget_manager, model_id, comm, init_state_callback) {
12 constructor: function (widget_manager, model_id, comm, init_state_callback) {
13 // Constructor
13 // Constructor
14 //
14 //
15 // Creates a WidgetModel instance.
15 // Creates a WidgetModel instance.
16 //
16 //
17 // Parameters
17 // Parameters
18 // ----------
18 // ----------
19 // widget_manager : WidgetManager instance
19 // widget_manager : WidgetManager instance
20 // model_id : string
20 // model_id : string
21 // An ID unique to this model.
21 // An ID unique to this model.
22 // comm : Comm instance (optional)
22 // comm : Comm instance (optional)
23 // init_state_callback : callback (optional)
23 // init_state_callback : callback (optional)
24 // Called once when the first state message is recieved from
24 // Called once when the first state message is recieved from
25 // the back-end.
25 // the back-end.
26 this.widget_manager = widget_manager;
26 this.widget_manager = widget_manager;
27 this.init_state_callback = init_state_callback;
27 this.init_state_callback = init_state_callback;
28 this._buffered_state_diff = {};
28 this._buffered_state_diff = {};
29 this.pending_msgs = 0;
29 this.pending_msgs = 0;
30 this.msg_buffer = null;
30 this.msg_buffer = null;
31 this.state_lock = null;
31 this.state_lock = null;
32 this.id = model_id;
32 this.id = model_id;
33 this.views = {};
33 this.views = {};
34
34
35 if (comm !== undefined) {
35 if (comm !== undefined) {
36 // Remember comm associated with the model.
36 // Remember comm associated with the model.
37 this.comm = comm;
37 this.comm = comm;
38 comm.model = this;
38 comm.model = this;
39
39
40 // Hook comm messages up to model.
40 // Hook comm messages up to model.
41 comm.on_close($.proxy(this._handle_comm_closed, this));
41 comm.on_close($.proxy(this._handle_comm_closed, this));
42 comm.on_msg($.proxy(this._handle_comm_msg, this));
42 comm.on_msg($.proxy(this._handle_comm_msg, this));
43 }
43 }
44 return Backbone.Model.apply(this);
44 return Backbone.Model.apply(this);
45 },
45 },
46
46
47 send: function (content, callbacks) {
47 send: function (content, callbacks) {
48 // Send a custom msg over the comm.
48 // Send a custom msg over the comm.
49 if (this.comm !== undefined) {
49 if (this.comm !== undefined) {
50 var data = {method: 'custom', content: content};
50 var data = {method: 'custom', content: content};
51 this.comm.send(data, callbacks);
51 this.comm.send(data, callbacks);
52 this.pending_msgs++;
52 this.pending_msgs++;
53 }
53 }
54 },
54 },
55
55
56 _handle_comm_closed: function (msg) {
56 _handle_comm_closed: function (msg) {
57 // Handle when a widget is closed.
57 // Handle when a widget is closed.
58 this.trigger('comm:close');
58 this.trigger('comm:close');
59 this.stopListening();
59 this.stopListening();
60 this.trigger('destroy', this);
60 this.trigger('destroy', this);
61 delete this.comm.model; // Delete ref so GC will collect widget model.
61 delete this.comm.model; // Delete ref so GC will collect widget model.
62 delete this.comm;
62 delete this.comm;
63 delete this.model_id; // Delete id from model so widget manager cleans up.
63 delete this.model_id; // Delete id from model so widget manager cleans up.
64 for (var id in this.views) {
64 for (var id in this.views) {
65 if (this.views.hasOwnProperty(id)) {
65 if (this.views.hasOwnProperty(id)) {
66 this.views[id].remove();
66 this.views[id].remove();
67 }
67 }
68 }
68 }
69 },
69 },
70
70
71 _handle_comm_msg: function (msg) {
71 _handle_comm_msg: function (msg) {
72 // Handle incoming comm msg.
72 // Handle incoming comm msg.
73 var method = msg.content.data.method;
73 var method = msg.content.data.method;
74 switch (method) {
74 switch (method) {
75 case 'update':
75 case 'update':
76 this.set_state(msg.content.data.state);
76 this.set_state(msg.content.data.state);
77 if (this.init_state_callback) {
77 if (this.init_state_callback) {
78 this.init_state_callback.apply(this, [this]);
78 this.init_state_callback.apply(this, [this]);
79 this.init_state_callback = null;
79 delete this.init_state_callback;
80 }
80 }
81 break;
81 break;
82 case 'custom':
82 case 'custom':
83 this.trigger('msg:custom', msg.content.data.content);
83 this.trigger('msg:custom', msg.content.data.content);
84 break;
84 break;
85 case 'display':
85 case 'display':
86 this.widget_manager.display_view(msg, this);
86 this.widget_manager.display_view(msg, this);
87 break;
87 break;
88 }
88 }
89 },
89 },
90
90
91 set_state: function (state) {
91 set_state: function (state) {
92 // Handle when a widget is updated via the python side.
92 // Handle when a widget is updated via the python side.
93 this.state_lock = state;
93 this.state_lock = state;
94 try {
94 try {
95 var that = this;
95 var that = this;
96 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
96 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
97 obj[key] = that._unpack_models(state[key]);
97 obj[key] = that._unpack_models(state[key]);
98 return obj;
98 return obj;
99 }, {})]);
99 }, {})]);
100 } finally {
100 } finally {
101 this.state_lock = null;
101 this.state_lock = null;
102 }
102 }
103 },
103 },
104
104
105 _handle_status: function (msg, callbacks) {
105 _handle_status: function (msg, callbacks) {
106 // Handle status msgs.
106 // Handle status msgs.
107
107
108 // execution_state : ('busy', 'idle', 'starting')
108 // execution_state : ('busy', 'idle', 'starting')
109 if (this.comm !== undefined) {
109 if (this.comm !== undefined) {
110 if (msg.content.execution_state ==='idle') {
110 if (msg.content.execution_state ==='idle') {
111 // Send buffer if this message caused another message to be
111 // Send buffer if this message caused another message to be
112 // throttled.
112 // throttled.
113 if (this.msg_buffer !== null &&
113 if (this.msg_buffer !== null &&
114 (this.get('msg_throttle') || 3) === this.pending_msgs) {
114 (this.get('msg_throttle') || 3) === this.pending_msgs) {
115 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
115 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
116 this.comm.send(data, callbacks);
116 this.comm.send(data, callbacks);
117 this.msg_buffer = null;
117 this.msg_buffer = null;
118 } else {
118 } else {
119 --this.pending_msgs;
119 --this.pending_msgs;
120 }
120 }
121 }
121 }
122 }
122 }
123 },
123 },
124
124
125 callbacks: function(view) {
125 callbacks: function(view) {
126 // Create msg callbacks for a comm msg.
126 // Create msg callbacks for a comm msg.
127 var callbacks = this.widget_manager.callbacks(view);
127 var callbacks = this.widget_manager.callbacks(view);
128
128
129 if (callbacks.iopub === undefined) {
129 if (callbacks.iopub === undefined) {
130 callbacks.iopub = {};
130 callbacks.iopub = {};
131 }
131 }
132
132
133 var that = this;
133 var that = this;
134 callbacks.iopub.status = function (msg) {
134 callbacks.iopub.status = function (msg) {
135 that._handle_status(msg, callbacks);
135 that._handle_status(msg, callbacks);
136 };
136 };
137 return callbacks;
137 return callbacks;
138 },
138 },
139
139
140 set: function(key, val, options) {
140 set: function(key, val, options) {
141 // Set a value.
141 // Set a value.
142 var return_value = WidgetModel.__super__.set.apply(this, arguments);
142 var return_value = WidgetModel.__super__.set.apply(this, arguments);
143
143
144 // Backbone only remembers the diff of the most recent set()
144 // Backbone only remembers the diff of the most recent set()
145 // operation. Calling set multiple times in a row results in a
145 // operation. Calling set multiple times in a row results in a
146 // loss of diff information. Here we keep our own running diff.
146 // loss of diff information. Here we keep our own running diff.
147 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
147 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
148 return return_value;
148 return return_value;
149 },
149 },
150
150
151 sync: function (method, model, options) {
151 sync: function (method, model, options) {
152 // Handle sync to the back-end. Called when a model.save() is called.
152 // Handle sync to the back-end. Called when a model.save() is called.
153
153
154 // Make sure a comm exists.
154 // Make sure a comm exists.
155 var error = options.error || function() {
155 var error = options.error || function() {
156 console.error('Backbone sync error:', arguments);
156 console.error('Backbone sync error:', arguments);
157 };
157 };
158 if (this.comm === undefined) {
158 if (this.comm === undefined) {
159 error();
159 error();
160 return false;
160 return false;
161 }
161 }
162
162
163 // Delete any key value pairs that the back-end already knows about.
163 // Delete any key value pairs that the back-end already knows about.
164 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
164 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
165 if (this.state_lock !== null) {
165 if (this.state_lock !== null) {
166 var keys = Object.keys(this.state_lock);
166 var keys = Object.keys(this.state_lock);
167 for (var i=0; i<keys.length; i++) {
167 for (var i=0; i<keys.length; i++) {
168 var key = keys[i];
168 var key = keys[i];
169 if (attrs[key] === this.state_lock[key]) {
169 if (attrs[key] === this.state_lock[key]) {
170 delete attrs[key];
170 delete attrs[key];
171 }
171 }
172 }
172 }
173 }
173 }
174
174
175 // Only sync if there are attributes to send to the back-end.
175 // Only sync if there are attributes to send to the back-end.
176 attrs = this._pack_models(attrs);
176 attrs = this._pack_models(attrs);
177 if (_.size(attrs) > 0) {
177 if (_.size(attrs) > 0) {
178
178
179 // If this message was sent via backbone itself, it will not
179 // If this message was sent via backbone itself, it will not
180 // have any callbacks. It's important that we create callbacks
180 // have any callbacks. It's important that we create callbacks
181 // so we can listen for status messages, etc...
181 // so we can listen for status messages, etc...
182 var callbacks = options.callbacks || this.callbacks();
182 var callbacks = options.callbacks || this.callbacks();
183
183
184 // Check throttle.
184 // Check throttle.
185 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
185 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
186 // The throttle has been exceeded, buffer the current msg so
186 // The throttle has been exceeded, buffer the current msg so
187 // it can be sent once the kernel has finished processing
187 // it can be sent once the kernel has finished processing
188 // some of the existing messages.
188 // some of the existing messages.
189
189
190 // Combine updates if it is a 'patch' sync, otherwise replace updates
190 // Combine updates if it is a 'patch' sync, otherwise replace updates
191 switch (method) {
191 switch (method) {
192 case 'patch':
192 case 'patch':
193 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
193 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
194 break;
194 break;
195 case 'update':
195 case 'update':
196 case 'create':
196 case 'create':
197 this.msg_buffer = attrs;
197 this.msg_buffer = attrs;
198 break;
198 break;
199 default:
199 default:
200 error();
200 error();
201 return false;
201 return false;
202 }
202 }
203 this.msg_buffer_callbacks = callbacks;
203 this.msg_buffer_callbacks = callbacks;
204
204
205 } else {
205 } else {
206 // We haven't exceeded the throttle, send the message like
206 // We haven't exceeded the throttle, send the message like
207 // normal.
207 // normal.
208 var data = {method: 'backbone', sync_data: attrs};
208 var data = {method: 'backbone', sync_data: attrs};
209 this.comm.send(data, callbacks);
209 this.comm.send(data, callbacks);
210 this.pending_msgs++;
210 this.pending_msgs++;
211 }
211 }
212 }
212 }
213 // Since the comm is a one-way communication, assume the message
213 // Since the comm is a one-way communication, assume the message
214 // arrived. Don't call success since we don't have a model back from the server
214 // arrived. Don't call success since we don't have a model back from the server
215 // this means we miss out on the 'sync' event.
215 // this means we miss out on the 'sync' event.
216 this._buffered_state_diff = {};
216 this._buffered_state_diff = {};
217 },
217 },
218
218
219 save_changes: function(callbacks) {
219 save_changes: function(callbacks) {
220 // Push this model's state to the back-end
220 // Push this model's state to the back-end
221 //
221 //
222 // This invokes a Backbone.Sync.
222 // This invokes a Backbone.Sync.
223 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
223 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
224 },
224 },
225
225
226 _pack_models: function(value) {
226 _pack_models: function(value) {
227 // Replace models with model ids recursively.
227 // Replace models with model ids recursively.
228 var that = this;
228 var that = this;
229 var packed;
229 var packed;
230 if (value instanceof Backbone.Model) {
230 if (value instanceof Backbone.Model) {
231 return "IPY_MODEL_" + value.id;
231 return "IPY_MODEL_" + value.id;
232
232
233 } else if ($.isArray(value)) {
233 } else if ($.isArray(value)) {
234 packed = [];
234 packed = [];
235 _.each(value, function(sub_value, key) {
235 _.each(value, function(sub_value, key) {
236 packed.push(that._pack_models(sub_value));
236 packed.push(that._pack_models(sub_value));
237 });
237 });
238 return packed;
238 return packed;
239
239
240 } else if (value instanceof Object) {
240 } else if (value instanceof Object) {
241 packed = {};
241 packed = {};
242 _.each(value, function(sub_value, key) {
242 _.each(value, function(sub_value, key) {
243 packed[key] = that._pack_models(sub_value);
243 packed[key] = that._pack_models(sub_value);
244 });
244 });
245 return packed;
245 return packed;
246
246
247 } else {
247 } else {
248 return value;
248 return value;
249 }
249 }
250 },
250 },
251
251
252 _unpack_models: function(value) {
252 _unpack_models: function(value) {
253 // Replace model ids with models recursively.
253 // Replace model ids with models recursively.
254 var that = this;
254 var that = this;
255 var unpacked;
255 var unpacked;
256 if ($.isArray(value)) {
256 if ($.isArray(value)) {
257 unpacked = [];
257 unpacked = [];
258 _.each(value, function(sub_value, key) {
258 _.each(value, function(sub_value, key) {
259 unpacked.push(that._unpack_models(sub_value));
259 unpacked.push(that._unpack_models(sub_value));
260 });
260 });
261 return unpacked;
261 return unpacked;
262
262
263 } else if (value instanceof Object) {
263 } else if (value instanceof Object) {
264 unpacked = {};
264 unpacked = {};
265 _.each(value, function(sub_value, key) {
265 _.each(value, function(sub_value, key) {
266 unpacked[key] = that._unpack_models(sub_value);
266 unpacked[key] = that._unpack_models(sub_value);
267 });
267 });
268 return unpacked;
268 return unpacked;
269
269
270 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
270 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
271 var model = this.widget_manager.get_model(value.slice(10, value.length));
271 var model = this.widget_manager.get_model(value.slice(10, value.length));
272 if (model) {
272 if (model) {
273 return model;
273 return model;
274 } else {
274 } else {
275 return value;
275 return value;
276 }
276 }
277 } else {
277 } else {
278 return value;
278 return value;
279 }
279 }
280 },
280 },
281
281
282 on_some_change: function(keys, callback, context) {
282 on_some_change: function(keys, callback, context) {
283 // on_some_change(["key1", "key2"], foo, context) differs from
283 // on_some_change(["key1", "key2"], foo, context) differs from
284 // on("change:key1 change:key2", foo, context).
284 // on("change:key1 change:key2", foo, context).
285 // If the widget attributes key1 and key2 are both modified,
285 // If the widget attributes key1 and key2 are both modified,
286 // the second form will result in foo being called twice
286 // the second form will result in foo being called twice
287 // while the first will call foo only once.
287 // while the first will call foo only once.
288 this.on('change', function() {
288 this.on('change', function() {
289 if (keys.some(this.hasChanged, this)) {
289 if (keys.some(this.hasChanged, this)) {
290 callback.apply(context);
290 callback.apply(context);
291 }
291 }
292 }, this);
292 }, this);
293
293
294 },
294 },
295 });
295 });
296 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
296 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
297
297
298
298
299 var WidgetView = Backbone.View.extend({
299 var WidgetView = Backbone.View.extend({
300 initialize: function(parameters) {
300 initialize: function(parameters) {
301 // Public constructor.
301 // Public constructor.
302 this.model.on('change',this.update,this);
302 this.model.on('change',this.update,this);
303 this.options = parameters.options;
303 this.options = parameters.options;
304 this.child_model_views = {};
304 this.child_model_views = {};
305 this.child_views = {};
305 this.child_views = {};
306 this.id = this.id || IPython.utils.uuid();
306 this.id = this.id || IPython.utils.uuid();
307 this.model.views[this.id] = this;
307 this.model.views[this.id] = this;
308 this.on('displayed', function() {
308 this.on('displayed', function() {
309 this.is_displayed = true;
309 this.is_displayed = true;
310 }, this);
310 }, this);
311 },
311 },
312
312
313 update: function(){
313 update: function(){
314 // Triggered on model change.
314 // Triggered on model change.
315 //
315 //
316 // Update view to be consistent with this.model
316 // Update view to be consistent with this.model
317 },
317 },
318
318
319 create_child_view: function(child_model, options) {
319 create_child_view: function(child_model, options) {
320 // Create and return a child view.
320 // Create and return a child view.
321 //
321 //
322 // -given a model and (optionally) a view name if the view name is
322 // -given a model and (optionally) a view name if the view name is
323 // not given, it defaults to the model's default view attribute.
323 // not given, it defaults to the model's default view attribute.
324
324
325 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
325 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
326 // it would be great to have the widget manager add the cell metadata
326 // it would be great to have the widget manager add the cell metadata
327 // to the subview without having to add it here.
327 // to the subview without having to add it here.
328 var that = this;
328 var that = this;
329 var old_callback = options.callback || function(view) {};
329 var old_callback = options.callback || function(view) {};
330 options = $.extend({ parent: this, callback: function(child_view) {
330 options = $.extend({ parent: this, callback: function(child_view) {
331 // Associate the view id with the model id.
331 // Associate the view id with the model id.
332 if (that.child_model_views[child_model.id] === undefined) {
332 if (that.child_model_views[child_model.id] === undefined) {
333 that.child_model_views[child_model.id] = [];
333 that.child_model_views[child_model.id] = [];
334 }
334 }
335 that.child_model_views[child_model.id].push(child_view.id);
335 that.child_model_views[child_model.id].push(child_view.id);
336
336
337 // Remember the view by id.
337 // Remember the view by id.
338 that.child_views[child_view.id] = child_view;
338 that.child_views[child_view.id] = child_view;
339 old_callback(child_view);
339 old_callback(child_view);
340 }}, options || {});
340 }}, options || {});
341
341
342 this.model.widget_manager.create_view(child_model, options);
342 this.model.widget_manager.create_view(child_model, options);
343 },
343 },
344
344
345 pop_child_view: function(child_model) {
345 pop_child_view: function(child_model) {
346 // Delete a child view that was previously created using create_child_view.
346 // Delete a child view that was previously created using create_child_view.
347 var view_ids = this.child_model_views[child_model.id];
347 var view_ids = this.child_model_views[child_model.id];
348 if (view_ids !== undefined) {
348 if (view_ids !== undefined) {
349
349
350 // Only delete the first view in the list.
350 // Only delete the first view in the list.
351 var view_id = view_ids[0];
351 var view_id = view_ids[0];
352 var view = this.child_views[view_id];
352 var view = this.child_views[view_id];
353 delete this.child_views[view_id];
353 delete this.child_views[view_id];
354 view_ids.splice(0,1);
354 view_ids.splice(0,1);
355 delete child_model.views[view_id];
355 delete child_model.views[view_id];
356
356
357 // Remove the view list specific to this model if it is empty.
357 // Remove the view list specific to this model if it is empty.
358 if (view_ids.length === 0) {
358 if (view_ids.length === 0) {
359 delete this.child_model_views[child_model.id];
359 delete this.child_model_views[child_model.id];
360 }
360 }
361 return view;
361 return view;
362 }
362 }
363 return null;
363 return null;
364 },
364 },
365
365
366 do_diff: function(old_list, new_list, removed_callback, added_callback) {
366 do_diff: function(old_list, new_list, removed_callback, added_callback) {
367 // Difference a changed list and call remove and add callbacks for
367 // Difference a changed list and call remove and add callbacks for
368 // each removed and added item in the new list.
368 // each removed and added item in the new list.
369 //
369 //
370 // Parameters
370 // Parameters
371 // ----------
371 // ----------
372 // old_list : array
372 // old_list : array
373 // new_list : array
373 // new_list : array
374 // removed_callback : Callback(item)
374 // removed_callback : Callback(item)
375 // Callback that is called for each item removed.
375 // Callback that is called for each item removed.
376 // added_callback : Callback(item)
376 // added_callback : Callback(item)
377 // Callback that is called for each item added.
377 // Callback that is called for each item added.
378
378
379 // Walk the lists until an unequal entry is found.
379 // Walk the lists until an unequal entry is found.
380 var i;
380 var i;
381 for (i = 0; i < new_list.length; i++) {
381 for (i = 0; i < new_list.length; i++) {
382 if (i >= old_list.length || new_list[i] !== old_list[i]) {
382 if (i >= old_list.length || new_list[i] !== old_list[i]) {
383 break;
383 break;
384 }
384 }
385 }
385 }
386
386
387 // Remove the non-matching items from the old list.
387 // Remove the non-matching items from the old list.
388 for (var j = i; j < old_list.length; j++) {
388 for (var j = i; j < old_list.length; j++) {
389 removed_callback(old_list[j]);
389 removed_callback(old_list[j]);
390 }
390 }
391
391
392 // Add the rest of the new list items.
392 // Add the rest of the new list items.
393 for (; i < new_list.length; i++) {
393 for (; i < new_list.length; i++) {
394 added_callback(new_list[i]);
394 added_callback(new_list[i]);
395 }
395 }
396 },
396 },
397
397
398 callbacks: function(){
398 callbacks: function(){
399 // Create msg callbacks for a comm msg.
399 // Create msg callbacks for a comm msg.
400 return this.model.callbacks(this);
400 return this.model.callbacks(this);
401 },
401 },
402
402
403 render: function(){
403 render: function(){
404 // Render the view.
404 // Render the view.
405 //
405 //
406 // By default, this is only called the first time the view is created
406 // By default, this is only called the first time the view is created
407 },
407 },
408
408
409 show: function(){
409 show: function(){
410 // Show the widget-area
410 // Show the widget-area
411 if (this.options && this.options.cell &&
411 if (this.options && this.options.cell &&
412 this.options.cell.widget_area !== undefined) {
412 this.options.cell.widget_area !== undefined) {
413 this.options.cell.widget_area.show();
413 this.options.cell.widget_area.show();
414 }
414 }
415 },
415 },
416
416
417 send: function (content) {
417 send: function (content) {
418 // Send a custom msg associated with this view.
418 // Send a custom msg associated with this view.
419 this.model.send(content, this.callbacks());
419 this.model.send(content, this.callbacks());
420 },
420 },
421
421
422 touch: function () {
422 touch: function () {
423 this.model.save_changes(this.callbacks());
423 this.model.save_changes(this.callbacks());
424 },
424 },
425
425
426 after_displayed: function (callback, context) {
426 after_displayed: function (callback, context) {
427 // Calls the callback right away is the view is already displayed
427 // Calls the callback right away is the view is already displayed
428 // otherwise, register the callback to the 'displayed' event.
428 // otherwise, register the callback to the 'displayed' event.
429 if (this.is_displayed) {
429 if (this.is_displayed) {
430 callback.apply(context);
430 callback.apply(context);
431 } else {
431 } else {
432 this.on('displayed', callback, context);
432 this.on('displayed', callback, context);
433 }
433 }
434 },
434 },
435 });
435 });
436
436
437
437
438 var DOMWidgetView = WidgetView.extend({
438 var DOMWidgetView = WidgetView.extend({
439 initialize: function (parameters) {
439 initialize: function (parameters) {
440 // Public constructor
440 // Public constructor
441 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
441 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
442 this.on('displayed', this.show, this);
442 this.on('displayed', this.show, this);
443 this.model.on('change:visible', this.update_visible, this);
443 this.model.on('change:visible', this.update_visible, this);
444 this.model.on('change:_css', this.update_css, this);
444 this.model.on('change:_css', this.update_css, this);
445
445
446 this.model.on('change:_dom_classes', function(model, new_classes) {
446 this.model.on('change:_dom_classes', function(model, new_classes) {
447 var old_classes = model.previous('_dom_classes');
447 var old_classes = model.previous('_dom_classes');
448 this.update_classes(old_classes, new_classes);
448 this.update_classes(old_classes, new_classes);
449 }, this);
449 }, this);
450
450
451 this.model.on('change:color', function (model, value) {
451 this.model.on('change:color', function (model, value) {
452 this.update_attr('color', value); }, this);
452 this.update_attr('color', value); }, this);
453
453
454 this.model.on('change:background_color', function (model, value) {
454 this.model.on('change:background_color', function (model, value) {
455 this.update_attr('background', value); }, this);
455 this.update_attr('background', value); }, this);
456
456
457 this.model.on('change:width', function (model, value) {
457 this.model.on('change:width', function (model, value) {
458 this.update_attr('width', value); }, this);
458 this.update_attr('width', value); }, this);
459
459
460 this.model.on('change:height', function (model, value) {
460 this.model.on('change:height', function (model, value) {
461 this.update_attr('height', value); }, this);
461 this.update_attr('height', value); }, this);
462
462
463 this.model.on('change:border_color', function (model, value) {
463 this.model.on('change:border_color', function (model, value) {
464 this.update_attr('border-color', value); }, this);
464 this.update_attr('border-color', value); }, this);
465
465
466 this.model.on('change:border_width', function (model, value) {
466 this.model.on('change:border_width', function (model, value) {
467 this.update_attr('border-width', value); }, this);
467 this.update_attr('border-width', value); }, this);
468
468
469 this.model.on('change:border_style', function (model, value) {
469 this.model.on('change:border_style', function (model, value) {
470 this.update_attr('border-style', value); }, this);
470 this.update_attr('border-style', value); }, this);
471
471
472 this.model.on('change:font_style', function (model, value) {
472 this.model.on('change:font_style', function (model, value) {
473 this.update_attr('font-style', value); }, this);
473 this.update_attr('font-style', value); }, this);
474
474
475 this.model.on('change:font_weight', function (model, value) {
475 this.model.on('change:font_weight', function (model, value) {
476 this.update_attr('font-weight', value); }, this);
476 this.update_attr('font-weight', value); }, this);
477
477
478 this.model.on('change:font_size', function (model, value) {
478 this.model.on('change:font_size', function (model, value) {
479 this.update_attr('font-size', this._default_px(value)); }, this);
479 this.update_attr('font-size', this._default_px(value)); }, this);
480
480
481 this.model.on('change:font_family', function (model, value) {
481 this.model.on('change:font_family', function (model, value) {
482 this.update_attr('font-family', value); }, this);
482 this.update_attr('font-family', value); }, this);
483
483
484 this.model.on('change:padding', function (model, value) {
484 this.model.on('change:padding', function (model, value) {
485 this.update_attr('padding', value); }, this);
485 this.update_attr('padding', value); }, this);
486
486
487 this.model.on('change:margin', function (model, value) {
487 this.model.on('change:margin', function (model, value) {
488 this.update_attr('margin', this._default_px(value)); }, this);
488 this.update_attr('margin', this._default_px(value)); }, this);
489
489
490 this.model.on('change:border_radius', function (model, value) {
490 this.model.on('change:border_radius', function (model, value) {
491 this.update_attr('border-radius', this._default_px(value)); }, this);
491 this.update_attr('border-radius', this._default_px(value)); }, this);
492
492
493 this.after_displayed(function() {
493 this.after_displayed(function() {
494 this.update_visible(this.model, this.model.get("visible"));
494 this.update_visible(this.model, this.model.get("visible"));
495 this.update_classes([], this.model.get('_dom_classes'));
495 this.update_classes([], this.model.get('_dom_classes'));
496
496
497 this.update_attr('color', this.model.get('color'));
497 this.update_attr('color', this.model.get('color'));
498 this.update_attr('background', this.model.get('background_color'));
498 this.update_attr('background', this.model.get('background_color'));
499 this.update_attr('width', this.model.get('width'));
499 this.update_attr('width', this.model.get('width'));
500 this.update_attr('height', this.model.get('height'));
500 this.update_attr('height', this.model.get('height'));
501 this.update_attr('border-color', this.model.get('border_color'));
501 this.update_attr('border-color', this.model.get('border_color'));
502 this.update_attr('border-width', this.model.get('border_width'));
502 this.update_attr('border-width', this.model.get('border_width'));
503 this.update_attr('border-style', this.model.get('border_style'));
503 this.update_attr('border-style', this.model.get('border_style'));
504 this.update_attr('font-style', this.model.get('font_style'));
504 this.update_attr('font-style', this.model.get('font_style'));
505 this.update_attr('font-weight', this.model.get('font_weight'));
505 this.update_attr('font-weight', this.model.get('font_weight'));
506 this.update_attr('font-size', this.model.get('font_size'));
506 this.update_attr('font-size', this.model.get('font_size'));
507 this.update_attr('font-family', this.model.get('font_family'));
507 this.update_attr('font-family', this.model.get('font_family'));
508 this.update_attr('padding', this.model.get('padding'));
508 this.update_attr('padding', this.model.get('padding'));
509 this.update_attr('margin', this.model.get('margin'));
509 this.update_attr('margin', this.model.get('margin'));
510 this.update_attr('border-radius', this.model.get('border_radius'));
510 this.update_attr('border-radius', this.model.get('border_radius'));
511
511
512 this.update_css(this.model, this.model.get("_css"));
512 this.update_css(this.model, this.model.get("_css"));
513 }, this);
513 }, this);
514 },
514 },
515
515
516 _default_px: function(value) {
516 _default_px: function(value) {
517 // Makes browser interpret a numerical string as a pixel value.
517 // Makes browser interpret a numerical string as a pixel value.
518 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
518 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
519 return value.trim() + 'px';
519 return value.trim() + 'px';
520 }
520 }
521 return value;
521 return value;
522 },
522 },
523
523
524 update_attr: function(name, value) {
524 update_attr: function(name, value) {
525 // Set a css attr of the widget view.
525 // Set a css attr of the widget view.
526 this.$el.css(name, value);
526 this.$el.css(name, value);
527 },
527 },
528
528
529 update_visible: function(model, value) {
529 update_visible: function(model, value) {
530 // Update visibility
530 // Update visibility
531 this.$el.toggle(value);
531 this.$el.toggle(value);
532 },
532 },
533
533
534 update_css: function (model, css) {
534 update_css: function (model, css) {
535 // Update the css styling of this view.
535 // Update the css styling of this view.
536 var e = this.$el;
536 var e = this.$el;
537 if (css === undefined) {return;}
537 if (css === undefined) {return;}
538 for (var i = 0; i < css.length; i++) {
538 for (var i = 0; i < css.length; i++) {
539 // Apply the css traits to all elements that match the selector.
539 // Apply the css traits to all elements that match the selector.
540 var selector = css[i][0];
540 var selector = css[i][0];
541 var elements = this._get_selector_element(selector);
541 var elements = this._get_selector_element(selector);
542 if (elements.length > 0) {
542 if (elements.length > 0) {
543 var trait_key = css[i][1];
543 var trait_key = css[i][1];
544 var trait_value = css[i][2];
544 var trait_value = css[i][2];
545 elements.css(trait_key ,trait_value);
545 elements.css(trait_key ,trait_value);
546 }
546 }
547 }
547 }
548 },
548 },
549
549
550 update_classes: function (old_classes, new_classes, $el) {
550 update_classes: function (old_classes, new_classes, $el) {
551 // Update the DOM classes applied to an element, default to this.$el.
551 // Update the DOM classes applied to an element, default to this.$el.
552 if ($el===undefined) {
552 if ($el===undefined) {
553 $el = this.$el;
553 $el = this.$el;
554 }
554 }
555 this.do_diff(old_classes, new_classes, function(removed) {
555 this.do_diff(old_classes, new_classes, function(removed) {
556 $el.removeClass(removed);
556 $el.removeClass(removed);
557 }, function(added) {
557 }, function(added) {
558 $el.addClass(added);
558 $el.addClass(added);
559 });
559 });
560 },
560 },
561
561
562 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
562 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
563 // Update the DOM classes applied to the widget based on a single
563 // Update the DOM classes applied to the widget based on a single
564 // trait's value.
564 // trait's value.
565 //
565 //
566 // Given a trait value classes map, this function automatically
566 // Given a trait value classes map, this function automatically
567 // handles applying the appropriate classes to the widget element
567 // handles applying the appropriate classes to the widget element
568 // and removing classes that are no longer valid.
568 // and removing classes that are no longer valid.
569 //
569 //
570 // Parameters
570 // Parameters
571 // ----------
571 // ----------
572 // class_map: dictionary
572 // class_map: dictionary
573 // Dictionary of trait values to class lists.
573 // Dictionary of trait values to class lists.
574 // Example:
574 // Example:
575 // {
575 // {
576 // success: ['alert', 'alert-success'],
576 // success: ['alert', 'alert-success'],
577 // info: ['alert', 'alert-info'],
577 // info: ['alert', 'alert-info'],
578 // warning: ['alert', 'alert-warning'],
578 // warning: ['alert', 'alert-warning'],
579 // danger: ['alert', 'alert-danger']
579 // danger: ['alert', 'alert-danger']
580 // };
580 // };
581 // trait_name: string
581 // trait_name: string
582 // Name of the trait to check the value of.
582 // Name of the trait to check the value of.
583 // previous_trait_value: optional string, default ''
583 // previous_trait_value: optional string, default ''
584 // Last trait value
584 // Last trait value
585 // $el: optional jQuery element handle, defaults to this.$el
585 // $el: optional jQuery element handle, defaults to this.$el
586 // Element that the classes are applied to.
586 // Element that the classes are applied to.
587 var key = previous_trait_value;
587 var key = previous_trait_value;
588 if (key === undefined) {
588 if (key === undefined) {
589 key = this.model.previous(trait_name);
589 key = this.model.previous(trait_name);
590 }
590 }
591 var old_classes = class_map[key] ? class_map[key] : [];
591 var old_classes = class_map[key] ? class_map[key] : [];
592 key = this.model.get(trait_name);
592 key = this.model.get(trait_name);
593 var new_classes = class_map[key] ? class_map[key] : [];
593 var new_classes = class_map[key] ? class_map[key] : [];
594
594
595 this.update_classes(old_classes, new_classes, $el || this.$el);
595 this.update_classes(old_classes, new_classes, $el || this.$el);
596 },
596 },
597
597
598 _get_selector_element: function (selector) {
598 _get_selector_element: function (selector) {
599 // Get the elements via the css selector.
599 // Get the elements via the css selector.
600 var elements;
600 var elements;
601 if (!selector) {
601 if (!selector) {
602 elements = this.$el;
602 elements = this.$el;
603 } else {
603 } else {
604 elements = this.$el.find(selector).addBack(selector);
604 elements = this.$el.find(selector).addBack(selector);
605 }
605 }
606 return elements;
606 return elements;
607 },
607 },
608 });
608 });
609
609
610
610
611 var widget = {
611 var widget = {
612 'WidgetModel': WidgetModel,
612 'WidgetModel': WidgetModel,
613 'WidgetView': WidgetView,
613 'WidgetView': WidgetView,
614 'DOMWidgetView': DOMWidgetView,
614 'DOMWidgetView': DOMWidgetView,
615 };
615 };
616
616
617 // For backwards compatability.
617 // For backwards compatability.
618 $.extend(IPython, widget);
618 $.extend(IPython, widget);
619
619
620 return widget;
620 return widget;
621 });
621 });
@@ -1,470 +1,469 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16 import collections
16 import collections
17
17
18 from IPython.core.getipython import get_ipython
18 from IPython.core.getipython import get_ipython
19 from IPython.kernel.comm import Comm
19 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
20 from IPython.config import LoggingConfigurable
21 from IPython.utils.importstring import import_item
21 from IPython.utils.importstring import import_item
22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 from IPython.utils.py3compat import string_types
24 from IPython.utils.py3compat import string_types
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Classes
27 # Classes
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 class CallbackDispatcher(LoggingConfigurable):
29 class CallbackDispatcher(LoggingConfigurable):
30 """A structure for registering and running callbacks"""
30 """A structure for registering and running callbacks"""
31 callbacks = List()
31 callbacks = List()
32
32
33 def __call__(self, *args, **kwargs):
33 def __call__(self, *args, **kwargs):
34 """Call all of the registered callbacks."""
34 """Call all of the registered callbacks."""
35 value = None
35 value = None
36 for callback in self.callbacks:
36 for callback in self.callbacks:
37 try:
37 try:
38 local_value = callback(*args, **kwargs)
38 local_value = callback(*args, **kwargs)
39 except Exception as e:
39 except Exception as e:
40 ip = get_ipython()
40 ip = get_ipython()
41 if ip is None:
41 if ip is None:
42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
43 else:
43 else:
44 ip.showtraceback()
44 ip.showtraceback()
45 else:
45 else:
46 value = local_value if local_value is not None else value
46 value = local_value if local_value is not None else value
47 return value
47 return value
48
48
49 def register_callback(self, callback, remove=False):
49 def register_callback(self, callback, remove=False):
50 """(Un)Register a callback
50 """(Un)Register a callback
51
51
52 Parameters
52 Parameters
53 ----------
53 ----------
54 callback: method handle
54 callback: method handle
55 Method to be registered or unregistered.
55 Method to be registered or unregistered.
56 remove=False: bool
56 remove=False: bool
57 Whether to unregister the callback."""
57 Whether to unregister the callback."""
58
58
59 # (Un)Register the callback.
59 # (Un)Register the callback.
60 if remove and callback in self.callbacks:
60 if remove and callback in self.callbacks:
61 self.callbacks.remove(callback)
61 self.callbacks.remove(callback)
62 elif not remove and callback not in self.callbacks:
62 elif not remove and callback not in self.callbacks:
63 self.callbacks.append(callback)
63 self.callbacks.append(callback)
64
64
65 def _show_traceback(method):
65 def _show_traceback(method):
66 """decorator for showing tracebacks in IPython"""
66 """decorator for showing tracebacks in IPython"""
67 def m(self, *args, **kwargs):
67 def m(self, *args, **kwargs):
68 try:
68 try:
69 return(method(self, *args, **kwargs))
69 return(method(self, *args, **kwargs))
70 except Exception as e:
70 except Exception as e:
71 ip = get_ipython()
71 ip = get_ipython()
72 if ip is None:
72 if ip is None:
73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
74 else:
74 else:
75 ip.showtraceback()
75 ip.showtraceback()
76 return m
76 return m
77
77
78 class Widget(LoggingConfigurable):
78 class Widget(LoggingConfigurable):
79 #-------------------------------------------------------------------------
79 #-------------------------------------------------------------------------
80 # Class attributes
80 # Class attributes
81 #-------------------------------------------------------------------------
81 #-------------------------------------------------------------------------
82 _widget_construction_callback = None
82 _widget_construction_callback = None
83 widgets = {}
83 widgets = {}
84
84
85 @staticmethod
85 @staticmethod
86 def on_widget_constructed(callback):
86 def on_widget_constructed(callback):
87 """Registers a callback to be called when a widget is constructed.
87 """Registers a callback to be called when a widget is constructed.
88
88
89 The callback must have the following signature:
89 The callback must have the following signature:
90 callback(widget)"""
90 callback(widget)"""
91 Widget._widget_construction_callback = callback
91 Widget._widget_construction_callback = callback
92
92
93 @staticmethod
93 @staticmethod
94 def _call_widget_constructed(widget):
94 def _call_widget_constructed(widget):
95 """Static method, called when a widget is constructed."""
95 """Static method, called when a widget is constructed."""
96 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
97 Widget._widget_construction_callback(widget)
97 Widget._widget_construction_callback(widget)
98
98
99 @staticmethod
99 @staticmethod
100 def handle_comm_opened(comm, msg):
100 def handle_comm_opened(comm, msg):
101 """Static method, called when a widget is constructed."""
101 """Static method, called when a widget is constructed."""
102 target_name = msg['content']['data']['target_name']
102 target_name = msg['content']['data']['target_name']
103 widget_class = import_item(target_name)
103 widget_class = import_item(target_name)
104 widget = widget_class(open_comm=False)
104 widget = widget_class(open_comm=False)
105 widget.set_comm(comm)
105 widget.comm = comm
106
106
107
107
108 #-------------------------------------------------------------------------
108 #-------------------------------------------------------------------------
109 # Traits
109 # Traits
110 #-------------------------------------------------------------------------
110 #-------------------------------------------------------------------------
111 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
111 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
112 in which to find _model_name. If empty, look in the global registry.""")
112 in which to find _model_name. If empty, look in the global registry.""")
113 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
113 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
114 registered in the front-end to create and sync this widget with.""")
114 registered in the front-end to create and sync this widget with.""")
115 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
115 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
116 If empty, look in the global registry.""", sync=True)
116 If empty, look in the global registry.""", sync=True)
117 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
117 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
118 to use to represent the widget.""", sync=True)
118 to use to represent the widget.""", sync=True)
119 comm = Instance('IPython.kernel.comm.Comm')
119 comm = Instance('IPython.kernel.comm.Comm')
120
120
121 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
121 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
122 front-end can send before receiving an idle msg from the back-end.""")
122 front-end can send before receiving an idle msg from the back-end.""")
123
123
124 version = Int(0, sync=True, help="""Widget's version""")
124 version = Int(0, sync=True, help="""Widget's version""")
125 keys = List()
125 keys = List()
126 def _keys_default(self):
126 def _keys_default(self):
127 return [name for name in self.traits(sync=True)]
127 return [name for name in self.traits(sync=True)]
128
128
129 _property_lock = Tuple((None, None))
129 _property_lock = Tuple((None, None))
130 _send_state_lock = Int(0)
130 _send_state_lock = Int(0)
131 _states_to_send = Set(allow_none=False)
131 _states_to_send = Set(allow_none=False)
132 _display_callbacks = Instance(CallbackDispatcher, ())
132 _display_callbacks = Instance(CallbackDispatcher, ())
133 _msg_callbacks = Instance(CallbackDispatcher, ())
133 _msg_callbacks = Instance(CallbackDispatcher, ())
134
134
135 #-------------------------------------------------------------------------
135 #-------------------------------------------------------------------------
136 # (Con/de)structor
136 # (Con/de)structor
137 #-------------------------------------------------------------------------
137 #-------------------------------------------------------------------------
138 def __init__(self, open_comm=True, **kwargs):
138 def __init__(self, open_comm=True, **kwargs):
139 """Public constructor"""
139 """Public constructor"""
140 self._model_id = kwargs.pop('model_id', None)
140 self._model_id = kwargs.pop('model_id', None)
141 super(Widget, self).__init__(**kwargs)
141 super(Widget, self).__init__(**kwargs)
142
142
143 Widget._call_widget_constructed(self)
143 Widget._call_widget_constructed(self)
144 if open_comm:
144 if open_comm:
145 self.open()
145 self.open()
146
146
147 def __del__(self):
147 def __del__(self):
148 """Object disposal"""
148 """Object disposal"""
149 self.close()
149 self.close()
150
150
151 #-------------------------------------------------------------------------
151 #-------------------------------------------------------------------------
152 # Properties
152 # Properties
153 #-------------------------------------------------------------------------
153 #-------------------------------------------------------------------------
154
154
155 def open(self):
155 def open(self):
156 """Open a comm to the frontend if one isn't already open."""
156 """Open a comm to the frontend if one isn't already open."""
157 if self.comm is None:
157 if self.comm is None:
158 args = dict(target_name='ipython.widget',
158 args = dict(target_name='ipython.widget',
159 data={'model_name': self._model_name,
159 data={'model_name': self._model_name,
160 'model_module': self._model_module})
160 'model_module': self._model_module})
161 if self._model_id is not None:
161 if self._model_id is not None:
162 args['comm_id'] = self._model_id
162 args['comm_id'] = self._model_id
163 self.set_comm(Comm(**args))
163 self.comm = Comm(**args)
164
164
165 def set_comm(self, comm):
165 def _comm_changed(self, name, new):
166 """Set's the comm of the widget."""
166 """Called when the comm is changed."""
167 self.comm = comm
167 self.comm = new
168 self._model_id = self.model_id
168 self._model_id = self.model_id
169
169
170 self.comm.on_msg(self._handle_msg)
170 self.comm.on_msg(self._handle_msg)
171 Widget.widgets[self.model_id] = self
171 Widget.widgets[self.model_id] = self
172
172
173 # first update
173 # first update
174 self.send_state()
174 self.send_state()
175
175
176
177 @property
176 @property
178 def model_id(self):
177 def model_id(self):
179 """Gets the model id of this widget.
178 """Gets the model id of this widget.
180
179
181 If a Comm doesn't exist yet, a Comm will be created automagically."""
180 If a Comm doesn't exist yet, a Comm will be created automagically."""
182 return self.comm.comm_id
181 return self.comm.comm_id
183
182
184 #-------------------------------------------------------------------------
183 #-------------------------------------------------------------------------
185 # Methods
184 # Methods
186 #-------------------------------------------------------------------------
185 #-------------------------------------------------------------------------
187
186
188 def close(self):
187 def close(self):
189 """Close method.
188 """Close method.
190
189
191 Closes the underlying comm.
190 Closes the underlying comm.
192 When the comm is closed, all of the widget views are automatically
191 When the comm is closed, all of the widget views are automatically
193 removed from the front-end."""
192 removed from the front-end."""
194 if self.comm is not None:
193 if self.comm is not None:
195 Widget.widgets.pop(self.model_id, None)
194 Widget.widgets.pop(self.model_id, None)
196 self.comm.close()
195 self.comm.close()
197 self.comm = None
196 self.comm = None
198
197
199 def send_state(self, key=None):
198 def send_state(self, key=None):
200 """Sends the widget state, or a piece of it, to the front-end.
199 """Sends the widget state, or a piece of it, to the front-end.
201
200
202 Parameters
201 Parameters
203 ----------
202 ----------
204 key : unicode, or iterable (optional)
203 key : unicode, or iterable (optional)
205 A single property's name or iterable of property names to sync with the front-end.
204 A single property's name or iterable of property names to sync with the front-end.
206 """
205 """
207 self._send({
206 self._send({
208 "method" : "update",
207 "method" : "update",
209 "state" : self.get_state(key=key)
208 "state" : self.get_state(key=key)
210 })
209 })
211
210
212 def get_state(self, key=None):
211 def get_state(self, key=None):
213 """Gets the widget state, or a piece of it.
212 """Gets the widget state, or a piece of it.
214
213
215 Parameters
214 Parameters
216 ----------
215 ----------
217 key : unicode or iterable (optional)
216 key : unicode or iterable (optional)
218 A single property's name or iterable of property names to get.
217 A single property's name or iterable of property names to get.
219 """
218 """
220 if key is None:
219 if key is None:
221 keys = self.keys
220 keys = self.keys
222 elif isinstance(key, string_types):
221 elif isinstance(key, string_types):
223 keys = [key]
222 keys = [key]
224 elif isinstance(key, collections.Iterable):
223 elif isinstance(key, collections.Iterable):
225 keys = key
224 keys = key
226 else:
225 else:
227 raise ValueError("key must be a string, an iterable of keys, or None")
226 raise ValueError("key must be a string, an iterable of keys, or None")
228 state = {}
227 state = {}
229 for k in keys:
228 for k in keys:
230 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
229 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
231 value = getattr(self, k)
230 value = getattr(self, k)
232 state[k] = f(value)
231 state[k] = f(value)
233 return state
232 return state
234
233
235 def set_state(self, sync_data):
234 def set_state(self, sync_data):
236 """Called when a state is received from the front-end."""
235 """Called when a state is received from the front-end."""
237 for name in self.keys:
236 for name in self.keys:
238 if name in sync_data:
237 if name in sync_data:
239 json_value = sync_data[name]
238 json_value = sync_data[name]
240 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
239 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
241 with self._lock_property(name, json_value):
240 with self._lock_property(name, json_value):
242 setattr(self, name, from_json(json_value))
241 setattr(self, name, from_json(json_value))
243
242
244 def send(self, content):
243 def send(self, content):
245 """Sends a custom msg to the widget model in the front-end.
244 """Sends a custom msg to the widget model in the front-end.
246
245
247 Parameters
246 Parameters
248 ----------
247 ----------
249 content : dict
248 content : dict
250 Content of the message to send.
249 Content of the message to send.
251 """
250 """
252 self._send({"method": "custom", "content": content})
251 self._send({"method": "custom", "content": content})
253
252
254 def on_msg(self, callback, remove=False):
253 def on_msg(self, callback, remove=False):
255 """(Un)Register a custom msg receive callback.
254 """(Un)Register a custom msg receive callback.
256
255
257 Parameters
256 Parameters
258 ----------
257 ----------
259 callback: callable
258 callback: callable
260 callback will be passed two arguments when a message arrives::
259 callback will be passed two arguments when a message arrives::
261
260
262 callback(widget, content)
261 callback(widget, content)
263
262
264 remove: bool
263 remove: bool
265 True if the callback should be unregistered."""
264 True if the callback should be unregistered."""
266 self._msg_callbacks.register_callback(callback, remove=remove)
265 self._msg_callbacks.register_callback(callback, remove=remove)
267
266
268 def on_displayed(self, callback, remove=False):
267 def on_displayed(self, callback, remove=False):
269 """(Un)Register a widget displayed callback.
268 """(Un)Register a widget displayed callback.
270
269
271 Parameters
270 Parameters
272 ----------
271 ----------
273 callback: method handler
272 callback: method handler
274 Must have a signature of::
273 Must have a signature of::
275
274
276 callback(widget, **kwargs)
275 callback(widget, **kwargs)
277
276
278 kwargs from display are passed through without modification.
277 kwargs from display are passed through without modification.
279 remove: bool
278 remove: bool
280 True if the callback should be unregistered."""
279 True if the callback should be unregistered."""
281 self._display_callbacks.register_callback(callback, remove=remove)
280 self._display_callbacks.register_callback(callback, remove=remove)
282
281
283 #-------------------------------------------------------------------------
282 #-------------------------------------------------------------------------
284 # Support methods
283 # Support methods
285 #-------------------------------------------------------------------------
284 #-------------------------------------------------------------------------
286 @contextmanager
285 @contextmanager
287 def _lock_property(self, key, value):
286 def _lock_property(self, key, value):
288 """Lock a property-value pair.
287 """Lock a property-value pair.
289
288
290 The value should be the JSON state of the property.
289 The value should be the JSON state of the property.
291
290
292 NOTE: This, in addition to the single lock for all state changes, is
291 NOTE: This, in addition to the single lock for all state changes, is
293 flawed. In the future we may want to look into buffering state changes
292 flawed. In the future we may want to look into buffering state changes
294 back to the front-end."""
293 back to the front-end."""
295 self._property_lock = (key, value)
294 self._property_lock = (key, value)
296 try:
295 try:
297 yield
296 yield
298 finally:
297 finally:
299 self._property_lock = (None, None)
298 self._property_lock = (None, None)
300
299
301 @contextmanager
300 @contextmanager
302 def hold_sync(self):
301 def hold_sync(self):
303 """Hold syncing any state until the context manager is released"""
302 """Hold syncing any state until the context manager is released"""
304 # We increment a value so that this can be nested. Syncing will happen when
303 # We increment a value so that this can be nested. Syncing will happen when
305 # all levels have been released.
304 # all levels have been released.
306 self._send_state_lock += 1
305 self._send_state_lock += 1
307 try:
306 try:
308 yield
307 yield
309 finally:
308 finally:
310 self._send_state_lock -=1
309 self._send_state_lock -=1
311 if self._send_state_lock == 0:
310 if self._send_state_lock == 0:
312 self.send_state(self._states_to_send)
311 self.send_state(self._states_to_send)
313 self._states_to_send.clear()
312 self._states_to_send.clear()
314
313
315 def _should_send_property(self, key, value):
314 def _should_send_property(self, key, value):
316 """Check the property lock (property_lock)"""
315 """Check the property lock (property_lock)"""
317 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
316 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
318 if (key == self._property_lock[0]
317 if (key == self._property_lock[0]
319 and to_json(value) == self._property_lock[1]):
318 and to_json(value) == self._property_lock[1]):
320 return False
319 return False
321 elif self._send_state_lock > 0:
320 elif self._send_state_lock > 0:
322 self._states_to_send.add(key)
321 self._states_to_send.add(key)
323 return False
322 return False
324 else:
323 else:
325 return True
324 return True
326
325
327 # Event handlers
326 # Event handlers
328 @_show_traceback
327 @_show_traceback
329 def _handle_msg(self, msg):
328 def _handle_msg(self, msg):
330 """Called when a msg is received from the front-end"""
329 """Called when a msg is received from the front-end"""
331 data = msg['content']['data']
330 data = msg['content']['data']
332 method = data['method']
331 method = data['method']
333 if not method in ['backbone', 'custom']:
332 if not method in ['backbone', 'custom']:
334 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
333 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
335
334
336 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
335 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
337 if method == 'backbone' and 'sync_data' in data:
336 if method == 'backbone' and 'sync_data' in data:
338 sync_data = data['sync_data']
337 sync_data = data['sync_data']
339 self.set_state(sync_data) # handles all methods
338 self.set_state(sync_data) # handles all methods
340
339
341 # Handle a custom msg from the front-end
340 # Handle a custom msg from the front-end
342 elif method == 'custom':
341 elif method == 'custom':
343 if 'content' in data:
342 if 'content' in data:
344 self._handle_custom_msg(data['content'])
343 self._handle_custom_msg(data['content'])
345
344
346 def _handle_custom_msg(self, content):
345 def _handle_custom_msg(self, content):
347 """Called when a custom msg is received."""
346 """Called when a custom msg is received."""
348 self._msg_callbacks(self, content)
347 self._msg_callbacks(self, content)
349
348
350 def _notify_trait(self, name, old_value, new_value):
349 def _notify_trait(self, name, old_value, new_value):
351 """Called when a property has been changed."""
350 """Called when a property has been changed."""
352 # Trigger default traitlet callback machinery. This allows any user
351 # Trigger default traitlet callback machinery. This allows any user
353 # registered validation to be processed prior to allowing the widget
352 # registered validation to be processed prior to allowing the widget
354 # machinery to handle the state.
353 # machinery to handle the state.
355 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
354 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
356
355
357 # Send the state after the user registered callbacks for trait changes
356 # Send the state after the user registered callbacks for trait changes
358 # have all fired (allows for user to validate values).
357 # have all fired (allows for user to validate values).
359 if self.comm is not None and name in self.keys:
358 if self.comm is not None and name in self.keys:
360 # Make sure this isn't information that the front-end just sent us.
359 # Make sure this isn't information that the front-end just sent us.
361 if self._should_send_property(name, new_value):
360 if self._should_send_property(name, new_value):
362 # Send new state to front-end
361 # Send new state to front-end
363 self.send_state(key=name)
362 self.send_state(key=name)
364
363
365 def _handle_displayed(self, **kwargs):
364 def _handle_displayed(self, **kwargs):
366 """Called when a view has been displayed for this widget instance"""
365 """Called when a view has been displayed for this widget instance"""
367 self._display_callbacks(self, **kwargs)
366 self._display_callbacks(self, **kwargs)
368
367
369 def _trait_to_json(self, x):
368 def _trait_to_json(self, x):
370 """Convert a trait value to json
369 """Convert a trait value to json
371
370
372 Traverse lists/tuples and dicts and serialize their values as well.
371 Traverse lists/tuples and dicts and serialize their values as well.
373 Replace any widgets with their model_id
372 Replace any widgets with their model_id
374 """
373 """
375 if isinstance(x, dict):
374 if isinstance(x, dict):
376 return {k: self._trait_to_json(v) for k, v in x.items()}
375 return {k: self._trait_to_json(v) for k, v in x.items()}
377 elif isinstance(x, (list, tuple)):
376 elif isinstance(x, (list, tuple)):
378 return [self._trait_to_json(v) for v in x]
377 return [self._trait_to_json(v) for v in x]
379 elif isinstance(x, Widget):
378 elif isinstance(x, Widget):
380 return "IPY_MODEL_" + x.model_id
379 return "IPY_MODEL_" + x.model_id
381 else:
380 else:
382 return x # Value must be JSON-able
381 return x # Value must be JSON-able
383
382
384 def _trait_from_json(self, x):
383 def _trait_from_json(self, x):
385 """Convert json values to objects
384 """Convert json values to objects
386
385
387 Replace any strings representing valid model id values to Widget references.
386 Replace any strings representing valid model id values to Widget references.
388 """
387 """
389 if isinstance(x, dict):
388 if isinstance(x, dict):
390 return {k: self._trait_from_json(v) for k, v in x.items()}
389 return {k: self._trait_from_json(v) for k, v in x.items()}
391 elif isinstance(x, (list, tuple)):
390 elif isinstance(x, (list, tuple)):
392 return [self._trait_from_json(v) for v in x]
391 return [self._trait_from_json(v) for v in x]
393 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
392 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
394 # we want to support having child widgets at any level in a hierarchy
393 # we want to support having child widgets at any level in a hierarchy
395 # trusting that a widget UUID will not appear out in the wild
394 # trusting that a widget UUID will not appear out in the wild
396 return Widget.widgets[x[10:]]
395 return Widget.widgets[x[10:]]
397 else:
396 else:
398 return x
397 return x
399
398
400 def _ipython_display_(self, **kwargs):
399 def _ipython_display_(self, **kwargs):
401 """Called when `IPython.display.display` is called on the widget."""
400 """Called when `IPython.display.display` is called on the widget."""
402 # Show view.
401 # Show view.
403 if self._view_name is not None:
402 if self._view_name is not None:
404 self._send({"method": "display"})
403 self._send({"method": "display"})
405 self._handle_displayed(**kwargs)
404 self._handle_displayed(**kwargs)
406
405
407 def _send(self, msg):
406 def _send(self, msg):
408 """Sends a message to the model in the front-end."""
407 """Sends a message to the model in the front-end."""
409 self.comm.send(msg)
408 self.comm.send(msg)
410
409
411
410
412 class DOMWidget(Widget):
411 class DOMWidget(Widget):
413 visible = Bool(True, help="Whether the widget is visible.", sync=True)
412 visible = Bool(True, help="Whether the widget is visible.", sync=True)
414 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
413 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
415 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
414 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
416
415
417 width = CUnicode(sync=True)
416 width = CUnicode(sync=True)
418 height = CUnicode(sync=True)
417 height = CUnicode(sync=True)
419 padding = CUnicode(sync=True)
418 padding = CUnicode(sync=True)
420 margin = CUnicode(sync=True)
419 margin = CUnicode(sync=True)
421
420
422 color = Unicode(sync=True)
421 color = Unicode(sync=True)
423 background_color = Unicode(sync=True)
422 background_color = Unicode(sync=True)
424 border_color = Unicode(sync=True)
423 border_color = Unicode(sync=True)
425
424
426 border_width = CUnicode(sync=True)
425 border_width = CUnicode(sync=True)
427 border_radius = CUnicode(sync=True)
426 border_radius = CUnicode(sync=True)
428 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
427 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
429 'none',
428 'none',
430 'hidden',
429 'hidden',
431 'dotted',
430 'dotted',
432 'dashed',
431 'dashed',
433 'solid',
432 'solid',
434 'double',
433 'double',
435 'groove',
434 'groove',
436 'ridge',
435 'ridge',
437 'inset',
436 'inset',
438 'outset',
437 'outset',
439 'initial',
438 'initial',
440 'inherit', ''],
439 'inherit', ''],
441 default_value='', sync=True)
440 default_value='', sync=True)
442
441
443 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
442 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
444 'normal',
443 'normal',
445 'italic',
444 'italic',
446 'oblique',
445 'oblique',
447 'initial',
446 'initial',
448 'inherit', ''],
447 'inherit', ''],
449 default_value='', sync=True)
448 default_value='', sync=True)
450 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
449 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
451 'normal',
450 'normal',
452 'bold',
451 'bold',
453 'bolder',
452 'bolder',
454 'lighter',
453 'lighter',
455 'initial',
454 'initial',
456 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
455 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
457 default_value='', sync=True)
456 default_value='', sync=True)
458 font_size = CUnicode(sync=True)
457 font_size = CUnicode(sync=True)
459 font_family = Unicode(sync=True)
458 font_family = Unicode(sync=True)
460
459
461 def __init__(self, *pargs, **kwargs):
460 def __init__(self, *pargs, **kwargs):
462 super(DOMWidget, self).__init__(*pargs, **kwargs)
461 super(DOMWidget, self).__init__(*pargs, **kwargs)
463
462
464 def _validate_border(name, old, new):
463 def _validate_border(name, old, new):
465 if new is not None and new != '':
464 if new is not None and new != '':
466 if name != 'border_width' and not self.border_width:
465 if name != 'border_width' and not self.border_width:
467 self.border_width = 1
466 self.border_width = 1
468 if name != 'border_style' and self.border_style == '':
467 if name != 'border_style' and self.border_style == '':
469 self.border_style = 'solid'
468 self.border_style = 'solid'
470 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
469 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now