##// END OF EJS Templates
Prepend a sentinel value to model ids to distinguish them from normal UUIDs (from Sylvain Corlay).
Jason Grout -
Show More
@@ -1,473 +1,475 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) {
12 constructor: function (widget_manager, model_id, comm) {
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 this.widget_manager = widget_manager;
23 this.widget_manager = widget_manager;
24 this._buffered_state_diff = {};
24 this._buffered_state_diff = {};
25 this.pending_msgs = 0;
25 this.pending_msgs = 0;
26 this.msg_buffer = null;
26 this.msg_buffer = null;
27 this.key_value_lock = null;
27 this.key_value_lock = null;
28 this.id = model_id;
28 this.id = model_id;
29 this.views = [];
29 this.views = [];
30
30
31 if (comm !== undefined) {
31 if (comm !== undefined) {
32 // Remember comm associated with the model.
32 // Remember comm associated with the model.
33 this.comm = comm;
33 this.comm = comm;
34 comm.model = this;
34 comm.model = this;
35
35
36 // Hook comm messages up to model.
36 // Hook comm messages up to model.
37 comm.on_close($.proxy(this._handle_comm_closed, this));
37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 comm.on_msg($.proxy(this._handle_comm_msg, this));
38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 }
39 }
40 return Backbone.Model.apply(this);
40 return Backbone.Model.apply(this);
41 },
41 },
42
42
43 send: function (content, callbacks) {
43 send: function (content, callbacks) {
44 // Send a custom msg over the comm.
44 // Send a custom msg over the comm.
45 if (this.comm !== undefined) {
45 if (this.comm !== undefined) {
46 var data = {method: 'custom', content: content};
46 var data = {method: 'custom', content: content};
47 this.comm.send(data, callbacks);
47 this.comm.send(data, callbacks);
48 this.pending_msgs++;
48 this.pending_msgs++;
49 }
49 }
50 },
50 },
51
51
52 _handle_comm_closed: function (msg) {
52 _handle_comm_closed: function (msg) {
53 // Handle when a widget is closed.
53 // Handle when a widget is closed.
54 this.trigger('comm:close');
54 this.trigger('comm:close');
55 delete this.comm.model; // Delete ref so GC will collect widget model.
55 delete this.comm.model; // Delete ref so GC will collect widget model.
56 delete this.comm;
56 delete this.comm;
57 delete this.model_id; // Delete id from model so widget manager cleans up.
57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 _.each(this.views, function(view, i) {
58 _.each(this.views, function(view, i) {
59 view.remove();
59 view.remove();
60 });
60 });
61 },
61 },
62
62
63 _handle_comm_msg: function (msg) {
63 _handle_comm_msg: function (msg) {
64 // Handle incoming comm msg.
64 // Handle incoming comm msg.
65 var method = msg.content.data.method;
65 var method = msg.content.data.method;
66 switch (method) {
66 switch (method) {
67 case 'update':
67 case 'update':
68 this.apply_update(msg.content.data.state);
68 this.apply_update(msg.content.data.state);
69 break;
69 break;
70 case 'custom':
70 case 'custom':
71 this.trigger('msg:custom', msg.content.data.content);
71 this.trigger('msg:custom', msg.content.data.content);
72 break;
72 break;
73 case 'display':
73 case 'display':
74 this.widget_manager.display_view(msg, this);
74 this.widget_manager.display_view(msg, this);
75 break;
75 break;
76 }
76 }
77 },
77 },
78
78
79 apply_update: function (state) {
79 apply_update: function (state) {
80 // Handle when a widget is updated via the python side.
80 // Handle when a widget is updated via the python side.
81 var that = this;
81 var that = this;
82 _.each(state, function(value, key) {
82 _.each(state, function(value, key) {
83 that.key_value_lock = [key, value];
83 that.key_value_lock = [key, value];
84 try {
84 try {
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
86 } finally {
86 } finally {
87 that.key_value_lock = null;
87 that.key_value_lock = null;
88 }
88 }
89 });
89 });
90 },
90 },
91
91
92 _handle_status: function (msg, callbacks) {
92 _handle_status: function (msg, callbacks) {
93 // Handle status msgs.
93 // Handle status msgs.
94
94
95 // execution_state : ('busy', 'idle', 'starting')
95 // execution_state : ('busy', 'idle', 'starting')
96 if (this.comm !== undefined) {
96 if (this.comm !== undefined) {
97 if (msg.content.execution_state ==='idle') {
97 if (msg.content.execution_state ==='idle') {
98 // Send buffer if this message caused another message to be
98 // Send buffer if this message caused another message to be
99 // throttled.
99 // throttled.
100 if (this.msg_buffer !== null &&
100 if (this.msg_buffer !== null &&
101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 this.comm.send(data, callbacks);
103 this.comm.send(data, callbacks);
104 this.msg_buffer = null;
104 this.msg_buffer = null;
105 } else {
105 } else {
106 --this.pending_msgs;
106 --this.pending_msgs;
107 }
107 }
108 }
108 }
109 }
109 }
110 },
110 },
111
111
112 callbacks: function(view) {
112 callbacks: function(view) {
113 // Create msg callbacks for a comm msg.
113 // Create msg callbacks for a comm msg.
114 var callbacks = this.widget_manager.callbacks(view);
114 var callbacks = this.widget_manager.callbacks(view);
115
115
116 if (callbacks.iopub === undefined) {
116 if (callbacks.iopub === undefined) {
117 callbacks.iopub = {};
117 callbacks.iopub = {};
118 }
118 }
119
119
120 var that = this;
120 var that = this;
121 callbacks.iopub.status = function (msg) {
121 callbacks.iopub.status = function (msg) {
122 that._handle_status(msg, callbacks);
122 that._handle_status(msg, callbacks);
123 };
123 };
124 return callbacks;
124 return callbacks;
125 },
125 },
126
126
127 set: function(key, val, options) {
127 set: function(key, val, options) {
128 // Set a value.
128 // Set a value.
129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130
130
131 // Backbone only remembers the diff of the most recent set()
131 // Backbone only remembers the diff of the most recent set()
132 // operation. Calling set multiple times in a row results in a
132 // operation. Calling set multiple times in a row results in a
133 // loss of diff information. Here we keep our own running diff.
133 // loss of diff information. Here we keep our own running diff.
134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 return return_value;
135 return return_value;
136 },
136 },
137
137
138 sync: function (method, model, options) {
138 sync: function (method, model, options) {
139 // Handle sync to the back-end. Called when a model.save() is called.
139 // Handle sync to the back-end. Called when a model.save() is called.
140
140
141 // Make sure a comm exists.
141 // Make sure a comm exists.
142 var error = options.error || function() {
142 var error = options.error || function() {
143 console.error('Backbone sync error:', arguments);
143 console.error('Backbone sync error:', arguments);
144 };
144 };
145 if (this.comm === undefined) {
145 if (this.comm === undefined) {
146 error();
146 error();
147 return false;
147 return false;
148 }
148 }
149
149
150 // Delete any key value pairs that the back-end already knows about.
150 // Delete any key value pairs that the back-end already knows about.
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 if (this.key_value_lock !== null) {
152 if (this.key_value_lock !== null) {
153 var key = this.key_value_lock[0];
153 var key = this.key_value_lock[0];
154 var value = this.key_value_lock[1];
154 var value = this.key_value_lock[1];
155 if (attrs[key] === value) {
155 if (attrs[key] === value) {
156 delete attrs[key];
156 delete attrs[key];
157 }
157 }
158 }
158 }
159
159
160 // Only sync if there are attributes to send to the back-end.
160 // Only sync if there are attributes to send to the back-end.
161 attrs = this._pack_models(attrs);
161 attrs = this._pack_models(attrs);
162 if (_.size(attrs) > 0) {
162 if (_.size(attrs) > 0) {
163
163
164 // If this message was sent via backbone itself, it will not
164 // If this message was sent via backbone itself, it will not
165 // have any callbacks. It's important that we create callbacks
165 // have any callbacks. It's important that we create callbacks
166 // so we can listen for status messages, etc...
166 // so we can listen for status messages, etc...
167 var callbacks = options.callbacks || this.callbacks();
167 var callbacks = options.callbacks || this.callbacks();
168
168
169 // Check throttle.
169 // Check throttle.
170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 // The throttle has been exceeded, buffer the current msg so
171 // The throttle has been exceeded, buffer the current msg so
172 // it can be sent once the kernel has finished processing
172 // it can be sent once the kernel has finished processing
173 // some of the existing messages.
173 // some of the existing messages.
174
174
175 // Combine updates if it is a 'patch' sync, otherwise replace updates
175 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 switch (method) {
176 switch (method) {
177 case 'patch':
177 case 'patch':
178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 break;
179 break;
180 case 'update':
180 case 'update':
181 case 'create':
181 case 'create':
182 this.msg_buffer = attrs;
182 this.msg_buffer = attrs;
183 break;
183 break;
184 default:
184 default:
185 error();
185 error();
186 return false;
186 return false;
187 }
187 }
188 this.msg_buffer_callbacks = callbacks;
188 this.msg_buffer_callbacks = callbacks;
189
189
190 } else {
190 } else {
191 // We haven't exceeded the throttle, send the message like
191 // We haven't exceeded the throttle, send the message like
192 // normal.
192 // normal.
193 var data = {method: 'backbone', sync_data: attrs};
193 var data = {method: 'backbone', sync_data: attrs};
194 this.comm.send(data, callbacks);
194 this.comm.send(data, callbacks);
195 this.pending_msgs++;
195 this.pending_msgs++;
196 }
196 }
197 }
197 }
198 // Since the comm is a one-way communication, assume the message
198 // Since the comm is a one-way communication, assume the message
199 // arrived. Don't call success since we don't have a model back from the server
199 // arrived. Don't call success since we don't have a model back from the server
200 // this means we miss out on the 'sync' event.
200 // this means we miss out on the 'sync' event.
201 this._buffered_state_diff = {};
201 this._buffered_state_diff = {};
202 },
202 },
203
203
204 save_changes: function(callbacks) {
204 save_changes: function(callbacks) {
205 // Push this model's state to the back-end
205 // Push this model's state to the back-end
206 //
206 //
207 // This invokes a Backbone.Sync.
207 // This invokes a Backbone.Sync.
208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 },
209 },
210
210
211 _pack_models: function(value) {
211 _pack_models: function(value) {
212 // Replace models with model ids recursively.
212 // Replace models with model ids recursively.
213 var that = this;
213 var that = this;
214 var packed;
214 var packed;
215 if (value instanceof Backbone.Model) {
215 if (value instanceof Backbone.Model) {
216 return value.id;
216 return "IPY_MODEL_" + value.id;
217
217
218 } else if ($.isArray(value)) {
218 } else if ($.isArray(value)) {
219 packed = [];
219 packed = [];
220 _.each(value, function(sub_value, key) {
220 _.each(value, function(sub_value, key) {
221 packed.push(that._pack_models(sub_value));
221 packed.push(that._pack_models(sub_value));
222 });
222 });
223 return packed;
223 return packed;
224
224
225 } else if (value instanceof Object) {
225 } else if (value instanceof Object) {
226 packed = {};
226 packed = {};
227 _.each(value, function(sub_value, key) {
227 _.each(value, function(sub_value, key) {
228 packed[key] = that._pack_models(sub_value);
228 packed[key] = that._pack_models(sub_value);
229 });
229 });
230 return packed;
230 return packed;
231
231
232 } else {
232 } else {
233 return value;
233 return value;
234 }
234 }
235 },
235 },
236
236
237 _unpack_models: function(value) {
237 _unpack_models: function(value) {
238 // Replace model ids with models recursively.
238 // Replace model ids with models recursively.
239 var that = this;
239 var that = this;
240 var unpacked;
240 var unpacked;
241 if ($.isArray(value)) {
241 if ($.isArray(value)) {
242 unpacked = [];
242 unpacked = [];
243 _.each(value, function(sub_value, key) {
243 _.each(value, function(sub_value, key) {
244 unpacked.push(that._unpack_models(sub_value));
244 unpacked.push(that._unpack_models(sub_value));
245 });
245 });
246 return unpacked;
246 return unpacked;
247
247
248 } else if (value instanceof Object) {
248 } else if (value instanceof Object) {
249 unpacked = {};
249 unpacked = {};
250 _.each(value, function(sub_value, key) {
250 _.each(value, function(sub_value, key) {
251 unpacked[key] = that._unpack_models(sub_value);
251 unpacked[key] = that._unpack_models(sub_value);
252 });
252 });
253 return unpacked;
253 return unpacked;
254
254
255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 if (model) {
258 return model;
259 } else {
260 return value;
261 }
255 } else {
262 } else {
256 var model = this.widget_manager.get_model(value);
257 if (model) {
258 return model;
259 } else {
260 return value;
263 return value;
261 }
262 }
264 }
263 },
265 },
264
266
265 });
267 });
266 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
267
269
268
270
269 var WidgetView = Backbone.View.extend({
271 var WidgetView = Backbone.View.extend({
270 initialize: function(parameters) {
272 initialize: function(parameters) {
271 // Public constructor.
273 // Public constructor.
272 this.model.on('change',this.update,this);
274 this.model.on('change',this.update,this);
273 this.options = parameters.options;
275 this.options = parameters.options;
274 this.child_model_views = {};
276 this.child_model_views = {};
275 this.child_views = {};
277 this.child_views = {};
276 this.model.views.push(this);
278 this.model.views.push(this);
277 this.id = this.id || IPython.utils.uuid();
279 this.id = this.id || IPython.utils.uuid();
278 },
280 },
279
281
280 update: function(){
282 update: function(){
281 // Triggered on model change.
283 // Triggered on model change.
282 //
284 //
283 // Update view to be consistent with this.model
285 // Update view to be consistent with this.model
284 },
286 },
285
287
286 create_child_view: function(child_model, options) {
288 create_child_view: function(child_model, options) {
287 // Create and return a child view.
289 // Create and return a child view.
288 //
290 //
289 // -given a model and (optionally) a view name if the view name is
291 // -given a model and (optionally) a view name if the view name is
290 // not given, it defaults to the model's default view attribute.
292 // not given, it defaults to the model's default view attribute.
291
293
292 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
294 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
293 // it would be great to have the widget manager add the cell metadata
295 // it would be great to have the widget manager add the cell metadata
294 // to the subview without having to add it here.
296 // to the subview without having to add it here.
295 options = $.extend({ parent: this }, options || {});
297 options = $.extend({ parent: this }, options || {});
296 var child_view = this.model.widget_manager.create_view(child_model, options, this);
298 var child_view = this.model.widget_manager.create_view(child_model, options, this);
297
299
298 // Associate the view id with the model id.
300 // Associate the view id with the model id.
299 if (this.child_model_views[child_model.id] === undefined) {
301 if (this.child_model_views[child_model.id] === undefined) {
300 this.child_model_views[child_model.id] = [];
302 this.child_model_views[child_model.id] = [];
301 }
303 }
302 this.child_model_views[child_model.id].push(child_view.id);
304 this.child_model_views[child_model.id].push(child_view.id);
303
305
304 // Remember the view by id.
306 // Remember the view by id.
305 this.child_views[child_view.id] = child_view;
307 this.child_views[child_view.id] = child_view;
306 return child_view;
308 return child_view;
307 },
309 },
308
310
309 pop_child_view: function(child_model) {
311 pop_child_view: function(child_model) {
310 // Delete a child view that was previously created using create_child_view.
312 // Delete a child view that was previously created using create_child_view.
311 var view_ids = this.child_model_views[child_model.id];
313 var view_ids = this.child_model_views[child_model.id];
312 if (view_ids !== undefined) {
314 if (view_ids !== undefined) {
313
315
314 // Only delete the first view in the list.
316 // Only delete the first view in the list.
315 var view_id = view_ids[0];
317 var view_id = view_ids[0];
316 var view = this.child_views[view_id];
318 var view = this.child_views[view_id];
317 delete this.child_views[view_id];
319 delete this.child_views[view_id];
318 view_ids.splice(0,1);
320 view_ids.splice(0,1);
319 child_model.views.pop(view);
321 child_model.views.pop(view);
320
322
321 // Remove the view list specific to this model if it is empty.
323 // Remove the view list specific to this model if it is empty.
322 if (view_ids.length === 0) {
324 if (view_ids.length === 0) {
323 delete this.child_model_views[child_model.id];
325 delete this.child_model_views[child_model.id];
324 }
326 }
325 return view;
327 return view;
326 }
328 }
327 return null;
329 return null;
328 },
330 },
329
331
330 do_diff: function(old_list, new_list, removed_callback, added_callback) {
332 do_diff: function(old_list, new_list, removed_callback, added_callback) {
331 // Difference a changed list and call remove and add callbacks for
333 // Difference a changed list and call remove and add callbacks for
332 // each removed and added item in the new list.
334 // each removed and added item in the new list.
333 //
335 //
334 // Parameters
336 // Parameters
335 // ----------
337 // ----------
336 // old_list : array
338 // old_list : array
337 // new_list : array
339 // new_list : array
338 // removed_callback : Callback(item)
340 // removed_callback : Callback(item)
339 // Callback that is called for each item removed.
341 // Callback that is called for each item removed.
340 // added_callback : Callback(item)
342 // added_callback : Callback(item)
341 // Callback that is called for each item added.
343 // Callback that is called for each item added.
342
344
343 // Walk the lists until an unequal entry is found.
345 // Walk the lists until an unequal entry is found.
344 var i;
346 var i;
345 for (i = 0; i < new_list.length; i++) {
347 for (i = 0; i < new_list.length; i++) {
346 if (i < old_list.length || new_list[i] !== old_list[i]) {
348 if (i < old_list.length || new_list[i] !== old_list[i]) {
347 break;
349 break;
348 }
350 }
349 }
351 }
350
352
351 // Remove the non-matching items from the old list.
353 // Remove the non-matching items from the old list.
352 for (var j = i; j < old_list.length; j++) {
354 for (var j = i; j < old_list.length; j++) {
353 removed_callback(old_list[j]);
355 removed_callback(old_list[j]);
354 }
356 }
355
357
356 // Add the rest of the new list items.
358 // Add the rest of the new list items.
357 for (i; i < new_list.length; i++) {
359 for (i; i < new_list.length; i++) {
358 added_callback(new_list[i]);
360 added_callback(new_list[i]);
359 }
361 }
360 },
362 },
361
363
362 callbacks: function(){
364 callbacks: function(){
363 // Create msg callbacks for a comm msg.
365 // Create msg callbacks for a comm msg.
364 return this.model.callbacks(this);
366 return this.model.callbacks(this);
365 },
367 },
366
368
367 render: function(){
369 render: function(){
368 // Render the view.
370 // Render the view.
369 //
371 //
370 // By default, this is only called the first time the view is created
372 // By default, this is only called the first time the view is created
371 },
373 },
372
374
373 send: function (content) {
375 send: function (content) {
374 // Send a custom msg associated with this view.
376 // Send a custom msg associated with this view.
375 this.model.send(content, this.callbacks());
377 this.model.send(content, this.callbacks());
376 },
378 },
377
379
378 touch: function () {
380 touch: function () {
379 this.model.save_changes(this.callbacks());
381 this.model.save_changes(this.callbacks());
380 },
382 },
381 });
383 });
382
384
383
385
384 var DOMWidgetView = WidgetView.extend({
386 var DOMWidgetView = WidgetView.extend({
385 initialize: function (options) {
387 initialize: function (options) {
386 // Public constructor
388 // Public constructor
387
389
388 // In the future we may want to make changes more granular
390 // In the future we may want to make changes more granular
389 // (e.g., trigger on visible:change).
391 // (e.g., trigger on visible:change).
390 this.model.on('change', this.update, this);
392 this.model.on('change', this.update, this);
391 this.model.on('msg:custom', this.on_msg, this);
393 this.model.on('msg:custom', this.on_msg, this);
392 DOMWidgetView.__super__.initialize.apply(this, arguments);
394 DOMWidgetView.__super__.initialize.apply(this, arguments);
393 },
395 },
394
396
395 on_msg: function(msg) {
397 on_msg: function(msg) {
396 // Handle DOM specific msgs.
398 // Handle DOM specific msgs.
397 switch(msg.msg_type) {
399 switch(msg.msg_type) {
398 case 'add_class':
400 case 'add_class':
399 this.add_class(msg.selector, msg.class_list);
401 this.add_class(msg.selector, msg.class_list);
400 break;
402 break;
401 case 'remove_class':
403 case 'remove_class':
402 this.remove_class(msg.selector, msg.class_list);
404 this.remove_class(msg.selector, msg.class_list);
403 break;
405 break;
404 }
406 }
405 },
407 },
406
408
407 add_class: function (selector, class_list) {
409 add_class: function (selector, class_list) {
408 // Add a DOM class to an element.
410 // Add a DOM class to an element.
409 this._get_selector_element(selector).addClass(class_list);
411 this._get_selector_element(selector).addClass(class_list);
410 },
412 },
411
413
412 remove_class: function (selector, class_list) {
414 remove_class: function (selector, class_list) {
413 // Remove a DOM class from an element.
415 // Remove a DOM class from an element.
414 this._get_selector_element(selector).removeClass(class_list);
416 this._get_selector_element(selector).removeClass(class_list);
415 },
417 },
416
418
417 update: function () {
419 update: function () {
418 // Update the contents of this view
420 // Update the contents of this view
419 //
421 //
420 // Called when the model is changed. The model may have been
422 // Called when the model is changed. The model may have been
421 // changed by another view or by a state update from the back-end.
423 // changed by another view or by a state update from the back-end.
422 // The very first update seems to happen before the element is
424 // The very first update seems to happen before the element is
423 // finished rendering so we use setTimeout to give the element time
425 // finished rendering so we use setTimeout to give the element time
424 // to render
426 // to render
425 var e = this.$el;
427 var e = this.$el;
426 var visible = this.model.get('visible');
428 var visible = this.model.get('visible');
427 setTimeout(function() {e.toggle(visible);},0);
429 setTimeout(function() {e.toggle(visible);},0);
428
430
429 var css = this.model.get('_css');
431 var css = this.model.get('_css');
430 if (css === undefined) {return;}
432 if (css === undefined) {return;}
431 for (var i = 0; i < css.length; i++) {
433 for (var i = 0; i < css.length; i++) {
432 // Apply the css traits to all elements that match the selector.
434 // Apply the css traits to all elements that match the selector.
433 var selector = css[i][0];
435 var selector = css[i][0];
434 var elements = this._get_selector_element(selector);
436 var elements = this._get_selector_element(selector);
435 if (elements.length > 0) {
437 if (elements.length > 0) {
436 var trait_key = css[i][1];
438 var trait_key = css[i][1];
437 var trait_value = css[i][2];
439 var trait_value = css[i][2];
438 elements.css(trait_key ,trait_value);
440 elements.css(trait_key ,trait_value);
439 }
441 }
440 }
442 }
441 },
443 },
442
444
443 _get_selector_element: function (selector) {
445 _get_selector_element: function (selector) {
444 // Get the elements via the css selector.
446 // Get the elements via the css selector.
445
447
446 // If the selector is blank, apply the style to the $el_to_style
448 // If the selector is blank, apply the style to the $el_to_style
447 // element. If the $el_to_style element is not defined, use apply
449 // element. If the $el_to_style element is not defined, use apply
448 // the style to the view's element.
450 // the style to the view's element.
449 var elements;
451 var elements;
450 if (!selector) {
452 if (!selector) {
451 if (this.$el_to_style === undefined) {
453 if (this.$el_to_style === undefined) {
452 elements = this.$el;
454 elements = this.$el;
453 } else {
455 } else {
454 elements = this.$el_to_style;
456 elements = this.$el_to_style;
455 }
457 }
456 } else {
458 } else {
457 elements = this.$el.find(selector);
459 elements = this.$el.find(selector);
458 }
460 }
459 return elements;
461 return elements;
460 },
462 },
461 });
463 });
462
464
463 var widget = {
465 var widget = {
464 'WidgetModel': WidgetModel,
466 'WidgetModel': WidgetModel,
465 'WidgetView': WidgetView,
467 'WidgetView': WidgetView,
466 'DOMWidgetView': DOMWidgetView,
468 'DOMWidgetView': DOMWidgetView,
467 };
469 };
468
470
469 // For backwards compatability.
471 // For backwards compatability.
470 $.extend(IPython, widget);
472 $.extend(IPython, widget);
471
473
472 return widget;
474 return widget;
473 });
475 });
@@ -1,455 +1,455 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
16
17 from IPython.core.getipython import get_ipython
17 from IPython.core.getipython import get_ipython
18 from IPython.kernel.comm import Comm
18 from IPython.kernel.comm import Comm
19 from IPython.config import LoggingConfigurable
19 from IPython.config import LoggingConfigurable
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
20 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int
21 from IPython.utils.py3compat import string_types
21 from IPython.utils.py3compat import string_types
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Classes
24 # Classes
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 class CallbackDispatcher(LoggingConfigurable):
26 class CallbackDispatcher(LoggingConfigurable):
27 """A structure for registering and running callbacks"""
27 """A structure for registering and running callbacks"""
28 callbacks = List()
28 callbacks = List()
29
29
30 def __call__(self, *args, **kwargs):
30 def __call__(self, *args, **kwargs):
31 """Call all of the registered callbacks."""
31 """Call all of the registered callbacks."""
32 value = None
32 value = None
33 for callback in self.callbacks:
33 for callback in self.callbacks:
34 try:
34 try:
35 local_value = callback(*args, **kwargs)
35 local_value = callback(*args, **kwargs)
36 except Exception as e:
36 except Exception as e:
37 ip = get_ipython()
37 ip = get_ipython()
38 if ip is None:
38 if ip is None:
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
39 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
40 else:
40 else:
41 ip.showtraceback()
41 ip.showtraceback()
42 else:
42 else:
43 value = local_value if local_value is not None else value
43 value = local_value if local_value is not None else value
44 return value
44 return value
45
45
46 def register_callback(self, callback, remove=False):
46 def register_callback(self, callback, remove=False):
47 """(Un)Register a callback
47 """(Un)Register a callback
48
48
49 Parameters
49 Parameters
50 ----------
50 ----------
51 callback: method handle
51 callback: method handle
52 Method to be registered or unregistered.
52 Method to be registered or unregistered.
53 remove=False: bool
53 remove=False: bool
54 Whether to unregister the callback."""
54 Whether to unregister the callback."""
55
55
56 # (Un)Register the callback.
56 # (Un)Register the callback.
57 if remove and callback in self.callbacks:
57 if remove and callback in self.callbacks:
58 self.callbacks.remove(callback)
58 self.callbacks.remove(callback)
59 elif not remove and callback not in self.callbacks:
59 elif not remove and callback not in self.callbacks:
60 self.callbacks.append(callback)
60 self.callbacks.append(callback)
61
61
62 def _show_traceback(method):
62 def _show_traceback(method):
63 """decorator for showing tracebacks in IPython"""
63 """decorator for showing tracebacks in IPython"""
64 def m(self, *args, **kwargs):
64 def m(self, *args, **kwargs):
65 try:
65 try:
66 return(method(self, *args, **kwargs))
66 return(method(self, *args, **kwargs))
67 except Exception as e:
67 except Exception as e:
68 ip = get_ipython()
68 ip = get_ipython()
69 if ip is None:
69 if ip is None:
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
70 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
71 else:
71 else:
72 ip.showtraceback()
72 ip.showtraceback()
73 return m
73 return m
74
74
75 class Widget(LoggingConfigurable):
75 class Widget(LoggingConfigurable):
76 #-------------------------------------------------------------------------
76 #-------------------------------------------------------------------------
77 # Class attributes
77 # Class attributes
78 #-------------------------------------------------------------------------
78 #-------------------------------------------------------------------------
79 _widget_construction_callback = None
79 _widget_construction_callback = None
80 widgets = {}
80 widgets = {}
81
81
82 @staticmethod
82 @staticmethod
83 def on_widget_constructed(callback):
83 def on_widget_constructed(callback):
84 """Registers a callback to be called when a widget is constructed.
84 """Registers a callback to be called when a widget is constructed.
85
85
86 The callback must have the following signature:
86 The callback must have the following signature:
87 callback(widget)"""
87 callback(widget)"""
88 Widget._widget_construction_callback = callback
88 Widget._widget_construction_callback = callback
89
89
90 @staticmethod
90 @staticmethod
91 def _call_widget_constructed(widget):
91 def _call_widget_constructed(widget):
92 """Static method, called when a widget is constructed."""
92 """Static method, called when a widget is constructed."""
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
93 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
94 Widget._widget_construction_callback(widget)
94 Widget._widget_construction_callback(widget)
95
95
96 #-------------------------------------------------------------------------
96 #-------------------------------------------------------------------------
97 # Traits
97 # Traits
98 #-------------------------------------------------------------------------
98 #-------------------------------------------------------------------------
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
99 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
100 registered in the front-end to create and sync this widget with.""")
100 registered in the front-end to create and sync this widget with.""")
101 _view_name = Unicode(help="""Default view registered in the front-end
101 _view_name = Unicode(help="""Default view registered in the front-end
102 to use to represent the widget.""", sync=True)
102 to use to represent the widget.""", sync=True)
103 _comm = Instance('IPython.kernel.comm.Comm')
103 _comm = Instance('IPython.kernel.comm.Comm')
104
104
105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
105 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
106 front-end can send before receiving an idle msg from the back-end.""")
106 front-end can send before receiving an idle msg from the back-end.""")
107
107
108 keys = List()
108 keys = List()
109 def _keys_default(self):
109 def _keys_default(self):
110 return [name for name in self.traits(sync=True)]
110 return [name for name in self.traits(sync=True)]
111
111
112 _property_lock = Tuple((None, None))
112 _property_lock = Tuple((None, None))
113
113
114 _display_callbacks = Instance(CallbackDispatcher, ())
114 _display_callbacks = Instance(CallbackDispatcher, ())
115 _msg_callbacks = Instance(CallbackDispatcher, ())
115 _msg_callbacks = Instance(CallbackDispatcher, ())
116
116
117 #-------------------------------------------------------------------------
117 #-------------------------------------------------------------------------
118 # (Con/de)structor
118 # (Con/de)structor
119 #-------------------------------------------------------------------------
119 #-------------------------------------------------------------------------
120 def __init__(self, **kwargs):
120 def __init__(self, **kwargs):
121 """Public constructor"""
121 """Public constructor"""
122 super(Widget, self).__init__(**kwargs)
122 super(Widget, self).__init__(**kwargs)
123
123
124 self.on_trait_change(self._handle_property_changed, self.keys)
124 self.on_trait_change(self._handle_property_changed, self.keys)
125 Widget._call_widget_constructed(self)
125 Widget._call_widget_constructed(self)
126
126
127 def __del__(self):
127 def __del__(self):
128 """Object disposal"""
128 """Object disposal"""
129 self.close()
129 self.close()
130
130
131 #-------------------------------------------------------------------------
131 #-------------------------------------------------------------------------
132 # Properties
132 # Properties
133 #-------------------------------------------------------------------------
133 #-------------------------------------------------------------------------
134
134
135 @property
135 @property
136 def comm(self):
136 def comm(self):
137 """Gets the Comm associated with this widget.
137 """Gets the Comm associated with this widget.
138
138
139 If a Comm doesn't exist yet, a Comm will be created automagically."""
139 If a Comm doesn't exist yet, a Comm will be created automagically."""
140 if self._comm is None:
140 if self._comm is None:
141 # Create a comm.
141 # Create a comm.
142 self._comm = Comm(target_name=self._model_name)
142 self._comm = Comm(target_name=self._model_name)
143 self._comm.on_msg(self._handle_msg)
143 self._comm.on_msg(self._handle_msg)
144 self._comm.on_close(self._close)
144 self._comm.on_close(self._close)
145 Widget.widgets[self.model_id] = self
145 Widget.widgets[self.model_id] = self
146
146
147 # first update
147 # first update
148 self.send_state()
148 self.send_state()
149 return self._comm
149 return self._comm
150
150
151 @property
151 @property
152 def model_id(self):
152 def model_id(self):
153 """Gets the model id of this widget.
153 """Gets the model id of this widget.
154
154
155 If a Comm doesn't exist yet, a Comm will be created automagically."""
155 If a Comm doesn't exist yet, a Comm will be created automagically."""
156 return self.comm.comm_id
156 return self.comm.comm_id
157
157
158 #-------------------------------------------------------------------------
158 #-------------------------------------------------------------------------
159 # Methods
159 # Methods
160 #-------------------------------------------------------------------------
160 #-------------------------------------------------------------------------
161 def _close(self):
161 def _close(self):
162 """Private close - cleanup objects, registry entries"""
162 """Private close - cleanup objects, registry entries"""
163 del Widget.widgets[self.model_id]
163 del Widget.widgets[self.model_id]
164 self._comm = None
164 self._comm = None
165
165
166 def close(self):
166 def close(self):
167 """Close method.
167 """Close method.
168
168
169 Closes the widget which closes the underlying comm.
169 Closes the widget which closes the underlying comm.
170 When the comm is closed, all of the widget views are automatically
170 When the comm is closed, all of the widget views are automatically
171 removed from the front-end."""
171 removed from the front-end."""
172 if self._comm is not None:
172 if self._comm is not None:
173 self._comm.close()
173 self._comm.close()
174 self._close()
174 self._close()
175
175
176 def send_state(self, key=None):
176 def send_state(self, key=None):
177 """Sends the widget state, or a piece of it, to the front-end.
177 """Sends the widget state, or a piece of it, to the front-end.
178
178
179 Parameters
179 Parameters
180 ----------
180 ----------
181 key : unicode (optional)
181 key : unicode (optional)
182 A single property's name to sync with the front-end.
182 A single property's name to sync with the front-end.
183 """
183 """
184 self._send({
184 self._send({
185 "method" : "update",
185 "method" : "update",
186 "state" : self.get_state()
186 "state" : self.get_state()
187 })
187 })
188
188
189 def get_state(self, key=None):
189 def get_state(self, key=None):
190 """Gets the widget state, or a piece of it.
190 """Gets the widget state, or a piece of it.
191
191
192 Parameters
192 Parameters
193 ----------
193 ----------
194 key : unicode (optional)
194 key : unicode (optional)
195 A single property's name to get.
195 A single property's name to get.
196 """
196 """
197 keys = self.keys if key is None else [key]
197 keys = self.keys if key is None else [key]
198 state = {}
198 state = {}
199 for k in keys:
199 for k in keys:
200 f = self.trait_metadata(k, 'to_json')
200 f = self.trait_metadata(k, 'to_json')
201 value = getattr(self, k)
201 value = getattr(self, k)
202 if f is not None:
202 if f is not None:
203 state[k] = f(value)
203 state[k] = f(value)
204 else:
204 else:
205 state[k] = self._serialize_trait(value)
205 state[k] = self._serialize_trait(value)
206 return state
206 return state
207
207
208 def send(self, content):
208 def send(self, content):
209 """Sends a custom msg to the widget model in the front-end.
209 """Sends a custom msg to the widget model in the front-end.
210
210
211 Parameters
211 Parameters
212 ----------
212 ----------
213 content : dict
213 content : dict
214 Content of the message to send.
214 Content of the message to send.
215 """
215 """
216 self._send({"method": "custom", "content": content})
216 self._send({"method": "custom", "content": content})
217
217
218 def on_msg(self, callback, remove=False):
218 def on_msg(self, callback, remove=False):
219 """(Un)Register a custom msg receive callback.
219 """(Un)Register a custom msg receive callback.
220
220
221 Parameters
221 Parameters
222 ----------
222 ----------
223 callback: callable
223 callback: callable
224 callback will be passed two arguments when a message arrives::
224 callback will be passed two arguments when a message arrives::
225
225
226 callback(widget, content)
226 callback(widget, content)
227
227
228 remove: bool
228 remove: bool
229 True if the callback should be unregistered."""
229 True if the callback should be unregistered."""
230 self._msg_callbacks.register_callback(callback, remove=remove)
230 self._msg_callbacks.register_callback(callback, remove=remove)
231
231
232 def on_displayed(self, callback, remove=False):
232 def on_displayed(self, callback, remove=False):
233 """(Un)Register a widget displayed callback.
233 """(Un)Register a widget displayed callback.
234
234
235 Parameters
235 Parameters
236 ----------
236 ----------
237 callback: method handler
237 callback: method handler
238 Must have a signature of::
238 Must have a signature of::
239
239
240 callback(widget, **kwargs)
240 callback(widget, **kwargs)
241
241
242 kwargs from display are passed through without modification.
242 kwargs from display are passed through without modification.
243 remove: bool
243 remove: bool
244 True if the callback should be unregistered."""
244 True if the callback should be unregistered."""
245 self._display_callbacks.register_callback(callback, remove=remove)
245 self._display_callbacks.register_callback(callback, remove=remove)
246
246
247 #-------------------------------------------------------------------------
247 #-------------------------------------------------------------------------
248 # Support methods
248 # Support methods
249 #-------------------------------------------------------------------------
249 #-------------------------------------------------------------------------
250 @contextmanager
250 @contextmanager
251 def _lock_property(self, key, value):
251 def _lock_property(self, key, value):
252 """Lock a property-value pair.
252 """Lock a property-value pair.
253
253
254 NOTE: This, in addition to the single lock for all state changes, is
254 NOTE: This, in addition to the single lock for all state changes, is
255 flawed. In the future we may want to look into buffering state changes
255 flawed. In the future we may want to look into buffering state changes
256 back to the front-end."""
256 back to the front-end."""
257 self._property_lock = (key, value)
257 self._property_lock = (key, value)
258 try:
258 try:
259 yield
259 yield
260 finally:
260 finally:
261 self._property_lock = (None, None)
261 self._property_lock = (None, None)
262
262
263 def _should_send_property(self, key, value):
263 def _should_send_property(self, key, value):
264 """Check the property lock (property_lock)"""
264 """Check the property lock (property_lock)"""
265 return key != self._property_lock[0] or \
265 return key != self._property_lock[0] or \
266 value != self._property_lock[1]
266 value != self._property_lock[1]
267
267
268 # Event handlers
268 # Event handlers
269 @_show_traceback
269 @_show_traceback
270 def _handle_msg(self, msg):
270 def _handle_msg(self, msg):
271 """Called when a msg is received from the front-end"""
271 """Called when a msg is received from the front-end"""
272 data = msg['content']['data']
272 data = msg['content']['data']
273 method = data['method']
273 method = data['method']
274 if not method in ['backbone', 'custom']:
274 if not method in ['backbone', 'custom']:
275 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
275 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
276
276
277 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
277 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
278 if method == 'backbone' and 'sync_data' in data:
278 if method == 'backbone' and 'sync_data' in data:
279 sync_data = data['sync_data']
279 sync_data = data['sync_data']
280 self._handle_receive_state(sync_data) # handles all methods
280 self._handle_receive_state(sync_data) # handles all methods
281
281
282 # Handle a custom msg from the front-end
282 # Handle a custom msg from the front-end
283 elif method == 'custom':
283 elif method == 'custom':
284 if 'content' in data:
284 if 'content' in data:
285 self._handle_custom_msg(data['content'])
285 self._handle_custom_msg(data['content'])
286
286
287 def _handle_receive_state(self, sync_data):
287 def _handle_receive_state(self, sync_data):
288 """Called when a state is received from the front-end."""
288 """Called when a state is received from the front-end."""
289 for name in self.keys:
289 for name in self.keys:
290 if name in sync_data:
290 if name in sync_data:
291 f = self.trait_metadata(name, 'from_json')
291 f = self.trait_metadata(name, 'from_json')
292 if f is not None:
292 if f is not None:
293 value = f(sync_data[name])
293 value = f(sync_data[name])
294 else:
294 else:
295 value = self._unserialize_trait(sync_data[name])
295 value = self._unserialize_trait(sync_data[name])
296 with self._lock_property(name, value):
296 with self._lock_property(name, value):
297 setattr(self, name, value)
297 setattr(self, name, value)
298
298
299 def _handle_custom_msg(self, content):
299 def _handle_custom_msg(self, content):
300 """Called when a custom msg is received."""
300 """Called when a custom msg is received."""
301 self._msg_callbacks(self, content)
301 self._msg_callbacks(self, content)
302
302
303 def _handle_property_changed(self, name, old, new):
303 def _handle_property_changed(self, name, old, new):
304 """Called when a property has been changed."""
304 """Called when a property has been changed."""
305 # Make sure this isn't information that the front-end just sent us.
305 # Make sure this isn't information that the front-end just sent us.
306 if self._should_send_property(name, new):
306 if self._should_send_property(name, new):
307 # Send new state to front-end
307 # Send new state to front-end
308 self.send_state(key=name)
308 self.send_state(key=name)
309
309
310 def _handle_displayed(self, **kwargs):
310 def _handle_displayed(self, **kwargs):
311 """Called when a view has been displayed for this widget instance"""
311 """Called when a view has been displayed for this widget instance"""
312 self._display_callbacks(self, **kwargs)
312 self._display_callbacks(self, **kwargs)
313
313
314 def _serialize_trait(self, x):
314 def _serialize_trait(self, x):
315 """Serialize a trait value to json
315 """Serialize a trait value to json
316
316
317 Traverse lists/tuples and dicts and serialize their values as well.
317 Traverse lists/tuples and dicts and serialize their values as well.
318 Replace any widgets with their model_id
318 Replace any widgets with their model_id
319 """
319 """
320 if isinstance(x, dict):
320 if isinstance(x, dict):
321 return {k: self._serialize_trait(v) for k, v in x.items()}
321 return {k: self._serialize_trait(v) for k, v in x.items()}
322 elif isinstance(x, (list, tuple)):
322 elif isinstance(x, (list, tuple)):
323 return [self._serialize_trait(v) for v in x]
323 return [self._serialize_trait(v) for v in x]
324 elif isinstance(x, Widget):
324 elif isinstance(x, Widget):
325 return x.model_id
325 return "IPY_MODEL_" + x.model_id
326 else:
326 else:
327 return x # Value must be JSON-able
327 return x # Value must be JSON-able
328
328
329 def _unserialize_trait(self, x):
329 def _unserialize_trait(self, x):
330 """Convert json values to objects
330 """Convert json values to objects
331
331
332 We explicitly support converting valid string widget UUIDs to Widget references.
332 We explicitly support converting valid string widget UUIDs to Widget references.
333 """
333 """
334 if isinstance(x, dict):
334 if isinstance(x, dict):
335 return {k: self._unserialize_trait(v) for k, v in x.items()}
335 return {k: self._unserialize_trait(v) for k, v in x.items()}
336 elif isinstance(x, (list, tuple)):
336 elif isinstance(x, (list, tuple)):
337 return [self._unserialize_trait(v) for v in x]
337 return [self._unserialize_trait(v) for v in x]
338 elif isinstance(x, string_types) and x in Widget.widgets:
338 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
339 # we want to support having child widgets at any level in a hierarchy
339 # we want to support having child widgets at any level in a hierarchy
340 # trusting that a widget UUID will not appear out in the wild
340 # trusting that a widget UUID will not appear out in the wild
341 return Widget.widgets[x]
341 return Widget.widgets[x]
342 else:
342 else:
343 return x
343 return x
344
344
345 def _ipython_display_(self, **kwargs):
345 def _ipython_display_(self, **kwargs):
346 """Called when `IPython.display.display` is called on the widget."""
346 """Called when `IPython.display.display` is called on the widget."""
347 # Show view. By sending a display message, the comm is opened and the
347 # Show view. By sending a display message, the comm is opened and the
348 # initial state is sent.
348 # initial state is sent.
349 self._send({"method": "display"})
349 self._send({"method": "display"})
350 self._handle_displayed(**kwargs)
350 self._handle_displayed(**kwargs)
351
351
352 def _send(self, msg):
352 def _send(self, msg):
353 """Sends a message to the model in the front-end."""
353 """Sends a message to the model in the front-end."""
354 self.comm.send(msg)
354 self.comm.send(msg)
355
355
356
356
357 class DOMWidget(Widget):
357 class DOMWidget(Widget):
358 visible = Bool(True, help="Whether the widget is visible.", sync=True)
358 visible = Bool(True, help="Whether the widget is visible.", sync=True)
359 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
359 _css = List(sync=True) # Internal CSS property list: (selector, key, value)
360
360
361 def get_css(self, key, selector=""):
361 def get_css(self, key, selector=""):
362 """Get a CSS property of the widget.
362 """Get a CSS property of the widget.
363
363
364 Note: This function does not actually request the CSS from the
364 Note: This function does not actually request the CSS from the
365 front-end; Only properties that have been set with set_css can be read.
365 front-end; Only properties that have been set with set_css can be read.
366
366
367 Parameters
367 Parameters
368 ----------
368 ----------
369 key: unicode
369 key: unicode
370 CSS key
370 CSS key
371 selector: unicode (optional)
371 selector: unicode (optional)
372 JQuery selector used when the CSS key/value was set.
372 JQuery selector used when the CSS key/value was set.
373 """
373 """
374 if selector in self._css and key in self._css[selector]:
374 if selector in self._css and key in self._css[selector]:
375 return self._css[selector][key]
375 return self._css[selector][key]
376 else:
376 else:
377 return None
377 return None
378
378
379 def set_css(self, dict_or_key, value=None, selector=''):
379 def set_css(self, dict_or_key, value=None, selector=''):
380 """Set one or more CSS properties of the widget.
380 """Set one or more CSS properties of the widget.
381
381
382 This function has two signatures:
382 This function has two signatures:
383 - set_css(css_dict, selector='')
383 - set_css(css_dict, selector='')
384 - set_css(key, value, selector='')
384 - set_css(key, value, selector='')
385
385
386 Parameters
386 Parameters
387 ----------
387 ----------
388 css_dict : dict
388 css_dict : dict
389 CSS key/value pairs to apply
389 CSS key/value pairs to apply
390 key: unicode
390 key: unicode
391 CSS key
391 CSS key
392 value:
392 value:
393 CSS value
393 CSS value
394 selector: unicode (optional, kwarg only)
394 selector: unicode (optional, kwarg only)
395 JQuery selector to use to apply the CSS key/value. If no selector
395 JQuery selector to use to apply the CSS key/value. If no selector
396 is provided, an empty selector is used. An empty selector makes the
396 is provided, an empty selector is used. An empty selector makes the
397 front-end try to apply the css to a default element. The default
397 front-end try to apply the css to a default element. The default
398 element is an attribute unique to each view, which is a DOM element
398 element is an attribute unique to each view, which is a DOM element
399 of the view that should be styled with common CSS (see
399 of the view that should be styled with common CSS (see
400 `$el_to_style` in the Javascript code).
400 `$el_to_style` in the Javascript code).
401 """
401 """
402 if value is None:
402 if value is None:
403 css_dict = dict_or_key
403 css_dict = dict_or_key
404 else:
404 else:
405 css_dict = {dict_or_key: value}
405 css_dict = {dict_or_key: value}
406
406
407 for (key, value) in css_dict.items():
407 for (key, value) in css_dict.items():
408 # First remove the selector/key pair from the css list if it exists.
408 # First remove the selector/key pair from the css list if it exists.
409 # Then add the selector/key pair and new value to the bottom of the
409 # Then add the selector/key pair and new value to the bottom of the
410 # list.
410 # list.
411 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
411 self._css = [x for x in self._css if not (x[0]==selector and x[1]==key)]
412 self._css += [(selector, key, value)]
412 self._css += [(selector, key, value)]
413 self.send_state('_css')
413 self.send_state('_css')
414
414
415 def add_class(self, class_names, selector=""):
415 def add_class(self, class_names, selector=""):
416 """Add class[es] to a DOM element.
416 """Add class[es] to a DOM element.
417
417
418 Parameters
418 Parameters
419 ----------
419 ----------
420 class_names: unicode or list
420 class_names: unicode or list
421 Class name(s) to add to the DOM element(s).
421 Class name(s) to add to the DOM element(s).
422 selector: unicode (optional)
422 selector: unicode (optional)
423 JQuery selector to select the DOM element(s) that the class(es) will
423 JQuery selector to select the DOM element(s) that the class(es) will
424 be added to.
424 be added to.
425 """
425 """
426 class_list = class_names
426 class_list = class_names
427 if isinstance(class_list, (list, tuple)):
427 if isinstance(class_list, (list, tuple)):
428 class_list = ' '.join(class_list)
428 class_list = ' '.join(class_list)
429
429
430 self.send({
430 self.send({
431 "msg_type" : "add_class",
431 "msg_type" : "add_class",
432 "class_list" : class_list,
432 "class_list" : class_list,
433 "selector" : selector
433 "selector" : selector
434 })
434 })
435
435
436 def remove_class(self, class_names, selector=""):
436 def remove_class(self, class_names, selector=""):
437 """Remove class[es] from a DOM element.
437 """Remove class[es] from a DOM element.
438
438
439 Parameters
439 Parameters
440 ----------
440 ----------
441 class_names: unicode or list
441 class_names: unicode or list
442 Class name(s) to remove from the DOM element(s).
442 Class name(s) to remove from the DOM element(s).
443 selector: unicode (optional)
443 selector: unicode (optional)
444 JQuery selector to select the DOM element(s) that the class(es) will
444 JQuery selector to select the DOM element(s) that the class(es) will
445 be removed from.
445 be removed from.
446 """
446 """
447 class_list = class_names
447 class_list = class_names
448 if isinstance(class_list, (list, tuple)):
448 if isinstance(class_list, (list, tuple)):
449 class_list = ' '.join(class_list)
449 class_list = ' '.join(class_list)
450
450
451 self.send({
451 self.send({
452 "msg_type" : "remove_class",
452 "msg_type" : "remove_class",
453 "class_list" : class_list,
453 "class_list" : class_list,
454 "selector" : selector,
454 "selector" : selector,
455 })
455 })
General Comments 0
You need to be logged in to leave comments. Login now