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