##// END OF EJS Templates
Set default view to None and never display widget with no view
Sylvain Corlay -
Show More
@@ -1,610 +1,607 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 _.each(this.views, function(view, i) {
60 _.each(this.views, function(view, i) {
61 view.remove();
61 view.remove();
62 });
62 });
63 },
63 },
64
64
65 _handle_comm_msg: function (msg) {
65 _handle_comm_msg: function (msg) {
66 // Handle incoming comm msg.
66 // Handle incoming comm msg.
67 var method = msg.content.data.method;
67 var method = msg.content.data.method;
68 switch (method) {
68 switch (method) {
69 case 'update':
69 case 'update':
70 this.apply_update(msg.content.data.state);
70 this.apply_update(msg.content.data.state);
71 break;
71 break;
72 case 'custom':
72 case 'custom':
73 this.trigger('msg:custom', msg.content.data.content);
73 this.trigger('msg:custom', msg.content.data.content);
74 break;
74 break;
75 case 'display':
75 case 'display':
76 this.widget_manager.display_view(msg, this);
76 this.widget_manager.display_view(msg, this);
77 break;
77 break;
78 }
78 }
79 },
79 },
80
80
81 apply_update: function (state) {
81 apply_update: function (state) {
82 // Handle when a widget is updated via the python side.
82 // Handle when a widget is updated via the python side.
83 this.state_lock = state;
83 this.state_lock = state;
84 try {
84 try {
85 var that = this;
85 var that = this;
86 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
86 WidgetModel.__super__.set.apply(this, [Object.keys(state).reduce(function(obj, key) {
87 obj[key] = that._unpack_models(state[key]);
87 obj[key] = that._unpack_models(state[key]);
88 return obj;
88 return obj;
89 }, {})]);
89 }, {})]);
90 } finally {
90 } finally {
91 this.state_lock = null;
91 this.state_lock = null;
92 }
92 }
93 },
93 },
94
94
95 _handle_status: function (msg, callbacks) {
95 _handle_status: function (msg, callbacks) {
96 // Handle status msgs.
96 // Handle status msgs.
97
97
98 // execution_state : ('busy', 'idle', 'starting')
98 // execution_state : ('busy', 'idle', 'starting')
99 if (this.comm !== undefined) {
99 if (this.comm !== undefined) {
100 if (msg.content.execution_state ==='idle') {
100 if (msg.content.execution_state ==='idle') {
101 // Send buffer if this message caused another message to be
101 // Send buffer if this message caused another message to be
102 // throttled.
102 // throttled.
103 if (this.msg_buffer !== null &&
103 if (this.msg_buffer !== null &&
104 (this.get('msg_throttle') || 3) === this.pending_msgs) {
104 (this.get('msg_throttle') || 3) === this.pending_msgs) {
105 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
105 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
106 this.comm.send(data, callbacks);
106 this.comm.send(data, callbacks);
107 this.msg_buffer = null;
107 this.msg_buffer = null;
108 } else {
108 } else {
109 --this.pending_msgs;
109 --this.pending_msgs;
110 }
110 }
111 }
111 }
112 }
112 }
113 },
113 },
114
114
115 callbacks: function(view) {
115 callbacks: function(view) {
116 // Create msg callbacks for a comm msg.
116 // Create msg callbacks for a comm msg.
117 var callbacks = this.widget_manager.callbacks(view);
117 var callbacks = this.widget_manager.callbacks(view);
118
118
119 if (callbacks.iopub === undefined) {
119 if (callbacks.iopub === undefined) {
120 callbacks.iopub = {};
120 callbacks.iopub = {};
121 }
121 }
122
122
123 var that = this;
123 var that = this;
124 callbacks.iopub.status = function (msg) {
124 callbacks.iopub.status = function (msg) {
125 that._handle_status(msg, callbacks);
125 that._handle_status(msg, callbacks);
126 };
126 };
127 return callbacks;
127 return callbacks;
128 },
128 },
129
129
130 set: function(key, val, options) {
130 set: function(key, val, options) {
131 // Set a value.
131 // Set a value.
132 var return_value = WidgetModel.__super__.set.apply(this, arguments);
132 var return_value = WidgetModel.__super__.set.apply(this, arguments);
133
133
134 // Backbone only remembers the diff of the most recent set()
134 // Backbone only remembers the diff of the most recent set()
135 // operation. Calling set multiple times in a row results in a
135 // operation. Calling set multiple times in a row results in a
136 // loss of diff information. Here we keep our own running diff.
136 // loss of diff information. Here we keep our own running diff.
137 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
137 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
138 return return_value;
138 return return_value;
139 },
139 },
140
140
141 sync: function (method, model, options) {
141 sync: function (method, model, options) {
142 // Handle sync to the back-end. Called when a model.save() is called.
142 // Handle sync to the back-end. Called when a model.save() is called.
143
143
144 // Make sure a comm exists.
144 // Make sure a comm exists.
145 var error = options.error || function() {
145 var error = options.error || function() {
146 console.error('Backbone sync error:', arguments);
146 console.error('Backbone sync error:', arguments);
147 };
147 };
148 if (this.comm === undefined) {
148 if (this.comm === undefined) {
149 error();
149 error();
150 return false;
150 return false;
151 }
151 }
152
152
153 // Delete any key value pairs that the back-end already knows about.
153 // Delete any key value pairs that the back-end already knows about.
154 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
154 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
155 if (this.state_lock !== null) {
155 if (this.state_lock !== null) {
156 var keys = Object.keys(this.state_lock);
156 var keys = Object.keys(this.state_lock);
157 for (var i=0; i<keys.length; i++) {
157 for (var i=0; i<keys.length; i++) {
158 var key = keys[i];
158 var key = keys[i];
159 if (attrs[key] === this.state_lock[key]) {
159 if (attrs[key] === this.state_lock[key]) {
160 delete attrs[key];
160 delete attrs[key];
161 }
161 }
162 }
162 }
163 }
163 }
164
164
165 // Only sync if there are attributes to send to the back-end.
165 // Only sync if there are attributes to send to the back-end.
166 attrs = this._pack_models(attrs);
166 attrs = this._pack_models(attrs);
167 if (_.size(attrs) > 0) {
167 if (_.size(attrs) > 0) {
168
168
169 // If this message was sent via backbone itself, it will not
169 // If this message was sent via backbone itself, it will not
170 // have any callbacks. It's important that we create callbacks
170 // have any callbacks. It's important that we create callbacks
171 // so we can listen for status messages, etc...
171 // so we can listen for status messages, etc...
172 var callbacks = options.callbacks || this.callbacks();
172 var callbacks = options.callbacks || this.callbacks();
173
173
174 // Check throttle.
174 // Check throttle.
175 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
175 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
176 // The throttle has been exceeded, buffer the current msg so
176 // The throttle has been exceeded, buffer the current msg so
177 // it can be sent once the kernel has finished processing
177 // it can be sent once the kernel has finished processing
178 // some of the existing messages.
178 // some of the existing messages.
179
179
180 // Combine updates if it is a 'patch' sync, otherwise replace updates
180 // Combine updates if it is a 'patch' sync, otherwise replace updates
181 switch (method) {
181 switch (method) {
182 case 'patch':
182 case 'patch':
183 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
183 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
184 break;
184 break;
185 case 'update':
185 case 'update':
186 case 'create':
186 case 'create':
187 this.msg_buffer = attrs;
187 this.msg_buffer = attrs;
188 break;
188 break;
189 default:
189 default:
190 error();
190 error();
191 return false;
191 return false;
192 }
192 }
193 this.msg_buffer_callbacks = callbacks;
193 this.msg_buffer_callbacks = callbacks;
194
194
195 } else {
195 } else {
196 // We haven't exceeded the throttle, send the message like
196 // We haven't exceeded the throttle, send the message like
197 // normal.
197 // normal.
198 var data = {method: 'backbone', sync_data: attrs};
198 var data = {method: 'backbone', sync_data: attrs};
199 this.comm.send(data, callbacks);
199 this.comm.send(data, callbacks);
200 this.pending_msgs++;
200 this.pending_msgs++;
201 }
201 }
202 }
202 }
203 // Since the comm is a one-way communication, assume the message
203 // Since the comm is a one-way communication, assume the message
204 // arrived. Don't call success since we don't have a model back from the server
204 // arrived. Don't call success since we don't have a model back from the server
205 // this means we miss out on the 'sync' event.
205 // this means we miss out on the 'sync' event.
206 this._buffered_state_diff = {};
206 this._buffered_state_diff = {};
207 },
207 },
208
208
209 save_changes: function(callbacks) {
209 save_changes: function(callbacks) {
210 // Push this model's state to the back-end
210 // Push this model's state to the back-end
211 //
211 //
212 // This invokes a Backbone.Sync.
212 // This invokes a Backbone.Sync.
213 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
213 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
214 },
214 },
215
215
216 _pack_models: function(value) {
216 _pack_models: function(value) {
217 // Replace models with model ids recursively.
217 // Replace models with model ids recursively.
218 var that = this;
218 var that = this;
219 var packed;
219 var packed;
220 if (value instanceof Backbone.Model) {
220 if (value instanceof Backbone.Model) {
221 return "IPY_MODEL_" + value.id;
221 return "IPY_MODEL_" + value.id;
222
222
223 } else if ($.isArray(value)) {
223 } else if ($.isArray(value)) {
224 packed = [];
224 packed = [];
225 _.each(value, function(sub_value, key) {
225 _.each(value, function(sub_value, key) {
226 packed.push(that._pack_models(sub_value));
226 packed.push(that._pack_models(sub_value));
227 });
227 });
228 return packed;
228 return packed;
229
229
230 } else if (value instanceof Object) {
230 } else if (value instanceof Object) {
231 packed = {};
231 packed = {};
232 _.each(value, function(sub_value, key) {
232 _.each(value, function(sub_value, key) {
233 packed[key] = that._pack_models(sub_value);
233 packed[key] = that._pack_models(sub_value);
234 });
234 });
235 return packed;
235 return packed;
236
236
237 } else {
237 } else {
238 return value;
238 return value;
239 }
239 }
240 },
240 },
241
241
242 _unpack_models: function(value) {
242 _unpack_models: function(value) {
243 // Replace model ids with models recursively.
243 // Replace model ids with models recursively.
244 var that = this;
244 var that = this;
245 var unpacked;
245 var unpacked;
246 if ($.isArray(value)) {
246 if ($.isArray(value)) {
247 unpacked = [];
247 unpacked = [];
248 _.each(value, function(sub_value, key) {
248 _.each(value, function(sub_value, key) {
249 unpacked.push(that._unpack_models(sub_value));
249 unpacked.push(that._unpack_models(sub_value));
250 });
250 });
251 return unpacked;
251 return unpacked;
252
252
253 } else if (value instanceof Object) {
253 } else if (value instanceof Object) {
254 unpacked = {};
254 unpacked = {};
255 _.each(value, function(sub_value, key) {
255 _.each(value, function(sub_value, key) {
256 unpacked[key] = that._unpack_models(sub_value);
256 unpacked[key] = that._unpack_models(sub_value);
257 });
257 });
258 return unpacked;
258 return unpacked;
259
259
260 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
260 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
261 var model = this.widget_manager.get_model(value.slice(10, value.length));
261 var model = this.widget_manager.get_model(value.slice(10, value.length));
262 if (model) {
262 if (model) {
263 return model;
263 return model;
264 } else {
264 } else {
265 return value;
265 return value;
266 }
266 }
267 } else {
267 } else {
268 return value;
268 return value;
269 }
269 }
270 },
270 },
271
271
272 on_atomic_change: function(keys, callback, context) {
272 on_atomic_change: function(keys, callback, context) {
273 // on__atomic_change(["key1", "key2"], foo, context) differs from
273 // on__atomic_change(["key1", "key2"], foo, context) differs from
274 // on("change:key1 change:key2", foo, context).
274 // on("change:key1 change:key2", foo, context).
275 // If the widget attributes key1 and key2 are both modified,
275 // If the widget attributes key1 and key2 are both modified,
276 // the second form will result in foo being called twice
276 // the second form will result in foo being called twice
277 // while the first will call foo only once.
277 // while the first will call foo only once.
278 this.on('change', function() {
278 this.on('change', function() {
279 if (keys.some(this.hasChanged, this)) {
279 if (keys.some(this.hasChanged, this)) {
280 callback.apply(context);
280 callback.apply(context);
281 }
281 }
282 }, this);
282 }, this);
283
283
284 },
284 },
285 });
285 });
286 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
286 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
287
287
288
288
289 var WidgetView = Backbone.View.extend({
289 var WidgetView = Backbone.View.extend({
290 initialize: function(parameters) {
290 initialize: function(parameters) {
291 // Public constructor.
291 // Public constructor.
292 this.model.on('change',this.update,this);
292 this.model.on('change',this.update,this);
293 this.options = parameters.options;
293 this.options = parameters.options;
294 this.child_model_views = {};
294 this.child_model_views = {};
295 this.child_views = {};
295 this.child_views = {};
296 this.model.views.push(this);
296 this.model.views.push(this);
297 this.id = this.id || IPython.utils.uuid();
297 this.id = this.id || IPython.utils.uuid();
298 this.on('displayed', function() {
298 this.on('displayed', function() {
299 this.is_displayed = true;
299 this.is_displayed = true;
300 }, this);
300 }, this);
301 },
301 },
302
302
303 update: function(){
303 update: function(){
304 // Triggered on model change.
304 // Triggered on model change.
305 //
305 //
306 // Update view to be consistent with this.model
306 // Update view to be consistent with this.model
307 },
307 },
308
308
309 create_child_view: function(child_model, options) {
309 create_child_view: function(child_model, options) {
310 // Create and return a child view.
310 // Create and return a child view.
311 //
311 //
312 // -given a model and (optionally) a view name if the view name is
312 // -given a model and (optionally) a view name if the view name is
313 // not given, it defaults to the model's default view attribute.
313 // not given, it defaults to the model's default view attribute.
314
314
315 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
315 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
316 // it would be great to have the widget manager add the cell metadata
316 // it would be great to have the widget manager add the cell metadata
317 // to the subview without having to add it here.
317 // to the subview without having to add it here.
318 options = $.extend({ parent: this }, options || {});
318 options = $.extend({ parent: this }, options || {});
319 var child_view = this.model.widget_manager.create_view(child_model, options, this);
319 var child_view = this.model.widget_manager.create_view(child_model, options, this);
320
320
321 // Associate the view id with the model id.
321 // Associate the view id with the model id.
322 if (this.child_model_views[child_model.id] === undefined) {
322 if (this.child_model_views[child_model.id] === undefined) {
323 this.child_model_views[child_model.id] = [];
323 this.child_model_views[child_model.id] = [];
324 }
324 }
325 this.child_model_views[child_model.id].push(child_view.id);
325 this.child_model_views[child_model.id].push(child_view.id);
326
326
327 // Remember the view by id.
327 // Remember the view by id.
328 this.child_views[child_view.id] = child_view;
328 this.child_views[child_view.id] = child_view;
329 return child_view;
329 return child_view;
330 },
330 },
331
331
332 pop_child_view: function(child_model) {
332 pop_child_view: function(child_model) {
333 // Delete a child view that was previously created using create_child_view.
333 // Delete a child view that was previously created using create_child_view.
334 var view_ids = this.child_model_views[child_model.id];
334 var view_ids = this.child_model_views[child_model.id];
335 if (view_ids !== undefined) {
335 if (view_ids !== undefined) {
336
336
337 // Only delete the first view in the list.
337 // Only delete the first view in the list.
338 var view_id = view_ids[0];
338 var view_id = view_ids[0];
339 var view = this.child_views[view_id];
339 var view = this.child_views[view_id];
340 delete this.child_views[view_id];
340 delete this.child_views[view_id];
341 view_ids.splice(0,1);
341 view_ids.splice(0,1);
342 child_model.views.pop(view);
342 child_model.views.pop(view);
343
343
344 // Remove the view list specific to this model if it is empty.
344 // Remove the view list specific to this model if it is empty.
345 if (view_ids.length === 0) {
345 if (view_ids.length === 0) {
346 delete this.child_model_views[child_model.id];
346 delete this.child_model_views[child_model.id];
347 }
347 }
348 return view;
348 return view;
349 }
349 }
350 return null;
350 return null;
351 },
351 },
352
352
353 do_diff: function(old_list, new_list, removed_callback, added_callback) {
353 do_diff: function(old_list, new_list, removed_callback, added_callback) {
354 // Difference a changed list and call remove and add callbacks for
354 // Difference a changed list and call remove and add callbacks for
355 // each removed and added item in the new list.
355 // each removed and added item in the new list.
356 //
356 //
357 // Parameters
357 // Parameters
358 // ----------
358 // ----------
359 // old_list : array
359 // old_list : array
360 // new_list : array
360 // new_list : array
361 // removed_callback : Callback(item)
361 // removed_callback : Callback(item)
362 // Callback that is called for each item removed.
362 // Callback that is called for each item removed.
363 // added_callback : Callback(item)
363 // added_callback : Callback(item)
364 // Callback that is called for each item added.
364 // Callback that is called for each item added.
365
365
366 // Walk the lists until an unequal entry is found.
366 // Walk the lists until an unequal entry is found.
367 var i;
367 var i;
368 for (i = 0; i < new_list.length; i++) {
368 for (i = 0; i < new_list.length; i++) {
369 if (i >= old_list.length || new_list[i] !== old_list[i]) {
369 if (i >= old_list.length || new_list[i] !== old_list[i]) {
370 break;
370 break;
371 }
371 }
372 }
372 }
373
373
374 // Remove the non-matching items from the old list.
374 // Remove the non-matching items from the old list.
375 for (var j = i; j < old_list.length; j++) {
375 for (var j = i; j < old_list.length; j++) {
376 removed_callback(old_list[j]);
376 removed_callback(old_list[j]);
377 }
377 }
378
378
379 // Add the rest of the new list items.
379 // Add the rest of the new list items.
380 for (; i < new_list.length; i++) {
380 for (; i < new_list.length; i++) {
381 added_callback(new_list[i]);
381 added_callback(new_list[i]);
382 }
382 }
383 },
383 },
384
384
385 callbacks: function(){
385 callbacks: function(){
386 // Create msg callbacks for a comm msg.
386 // Create msg callbacks for a comm msg.
387 return this.model.callbacks(this);
387 return this.model.callbacks(this);
388 },
388 },
389
389
390 render: function(){
390 render: function(){
391 // Render the view.
391 // Render the view.
392 //
392 //
393 // By default, this is only called the first time the view is created
393 // By default, this is only called the first time the view is created
394 },
394 },
395
395
396 show: function(){
396 show: function(){
397 // Show the widget-area
397 // Show the widget-area
398 if (this.options && this.options.cell &&
398 if (this.options && this.options.cell &&
399 this.options.cell.widget_area !== undefined) {
399 this.options.cell.widget_area !== undefined) {
400 this.options.cell.widget_area.show();
400 this.options.cell.widget_area.show();
401 }
401 }
402 },
402 },
403
403
404 send: function (content) {
404 send: function (content) {
405 // Send a custom msg associated with this view.
405 // Send a custom msg associated with this view.
406 this.model.send(content, this.callbacks());
406 this.model.send(content, this.callbacks());
407 },
407 },
408
408
409 touch: function () {
409 touch: function () {
410 this.model.save_changes(this.callbacks());
410 this.model.save_changes(this.callbacks());
411 },
411 },
412
412
413 after_displayed: function (callback, context) {
413 after_displayed: function (callback, context) {
414 // Calls the callback right away is the view is already displayed
414 // Calls the callback right away is the view is already displayed
415 // otherwise, register the callback to the 'displayed' event.
415 // otherwise, register the callback to the 'displayed' event.
416 if (this.is_displayed) {
416 if (this.is_displayed) {
417 callback.apply(context);
417 callback.apply(context);
418 } else {
418 } else {
419 this.on('displayed', callback, context);
419 this.on('displayed', callback, context);
420 }
420 }
421 },
421 },
422 });
422 });
423
423
424
424
425 widgetmanager.WidgetManager.register_widget_view('WidgetView', WidgetView);
426
427 var DOMWidgetView = WidgetView.extend({
425 var DOMWidgetView = WidgetView.extend({
428 initialize: function (parameters) {
426 initialize: function (parameters) {
429 // Public constructor
427 // Public constructor
430 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
428 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
431 this.on('displayed', this.show, this);
429 this.on('displayed', this.show, this);
432 this.model.on('change:visible', this.update_visible, this);
430 this.model.on('change:visible', this.update_visible, this);
433 this.model.on('change:_css', this.update_css, this);
431 this.model.on('change:_css', this.update_css, this);
434
432
435 this.model.on('change:_dom_classes', function(model, new_classes) {
433 this.model.on('change:_dom_classes', function(model, new_classes) {
436 var old_classes = model.previous('_dom_classes');
434 var old_classes = model.previous('_dom_classes');
437 this.update_classes(old_classes, new_classes);
435 this.update_classes(old_classes, new_classes);
438 }, this);
436 }, this);
439
437
440 this.model.on('change:color', function (model, value) {
438 this.model.on('change:color', function (model, value) {
441 this.update_attr('color', value); }, this);
439 this.update_attr('color', value); }, this);
442
440
443 this.model.on('change:background_color', function (model, value) {
441 this.model.on('change:background_color', function (model, value) {
444 this.update_attr('background', value); }, this);
442 this.update_attr('background', value); }, this);
445
443
446 this.model.on('change:width', function (model, value) {
444 this.model.on('change:width', function (model, value) {
447 this.update_attr('width', value); }, this);
445 this.update_attr('width', value); }, this);
448
446
449 this.model.on('change:height', function (model, value) {
447 this.model.on('change:height', function (model, value) {
450 this.update_attr('height', value); }, this);
448 this.update_attr('height', value); }, this);
451
449
452 this.model.on('change:border_color', function (model, value) {
450 this.model.on('change:border_color', function (model, value) {
453 this.update_attr('border-color', value); }, this);
451 this.update_attr('border-color', value); }, this);
454
452
455 this.model.on('change:border_width', function (model, value) {
453 this.model.on('change:border_width', function (model, value) {
456 this.update_attr('border-width', value); }, this);
454 this.update_attr('border-width', value); }, this);
457
455
458 this.model.on('change:border_style', function (model, value) {
456 this.model.on('change:border_style', function (model, value) {
459 this.update_attr('border-style', value); }, this);
457 this.update_attr('border-style', value); }, this);
460
458
461 this.model.on('change:font_style', function (model, value) {
459 this.model.on('change:font_style', function (model, value) {
462 this.update_attr('font-style', value); }, this);
460 this.update_attr('font-style', value); }, this);
463
461
464 this.model.on('change:font_weight', function (model, value) {
462 this.model.on('change:font_weight', function (model, value) {
465 this.update_attr('font-weight', value); }, this);
463 this.update_attr('font-weight', value); }, this);
466
464
467 this.model.on('change:font_size', function (model, value) {
465 this.model.on('change:font_size', function (model, value) {
468 this.update_attr('font-size', this._default_px(value)); }, this);
466 this.update_attr('font-size', this._default_px(value)); }, this);
469
467
470 this.model.on('change:font_family', function (model, value) {
468 this.model.on('change:font_family', function (model, value) {
471 this.update_attr('font-family', value); }, this);
469 this.update_attr('font-family', value); }, this);
472
470
473 this.model.on('change:padding', function (model, value) {
471 this.model.on('change:padding', function (model, value) {
474 this.update_attr('padding', value); }, this);
472 this.update_attr('padding', value); }, this);
475
473
476 this.model.on('change:margin', function (model, value) {
474 this.model.on('change:margin', function (model, value) {
477 this.update_attr('margin', this._default_px(value)); }, this);
475 this.update_attr('margin', this._default_px(value)); }, this);
478
476
479 this.model.on('change:border_radius', function (model, value) {
477 this.model.on('change:border_radius', function (model, value) {
480 this.update_attr('border-radius', this._default_px(value)); }, this);
478 this.update_attr('border-radius', this._default_px(value)); }, this);
481
479
482 this.after_displayed(function() {
480 this.after_displayed(function() {
483 this.update_visible(this.model, this.model.get("visible"));
481 this.update_visible(this.model, this.model.get("visible"));
484 this.update_css(this.model, this.model.get("_css"));
482 this.update_css(this.model, this.model.get("_css"));
485
483
486 this.update_classes([], this.model.get('_dom_classes'));
484 this.update_classes([], this.model.get('_dom_classes'));
487 this.update_attr('color', this.model.get('color'));
485 this.update_attr('color', this.model.get('color'));
488 this.update_attr('background', this.model.get('background_color'));
486 this.update_attr('background', this.model.get('background_color'));
489 this.update_attr('width', this.model.get('width'));
487 this.update_attr('width', this.model.get('width'));
490 this.update_attr('height', this.model.get('height'));
488 this.update_attr('height', this.model.get('height'));
491 this.update_attr('border-color', this.model.get('border_color'));
489 this.update_attr('border-color', this.model.get('border_color'));
492 this.update_attr('border-width', this.model.get('border_width'));
490 this.update_attr('border-width', this.model.get('border_width'));
493 this.update_attr('border-style', this.model.get('border_style'));
491 this.update_attr('border-style', this.model.get('border_style'));
494 this.update_attr('font-style', this.model.get('font_style'));
492 this.update_attr('font-style', this.model.get('font_style'));
495 this.update_attr('font-weight', this.model.get('font_weight'));
493 this.update_attr('font-weight', this.model.get('font_weight'));
496 this.update_attr('font-size', this.model.get('font_size'));
494 this.update_attr('font-size', this.model.get('font_size'));
497 this.update_attr('font-family', this.model.get('font_family'));
495 this.update_attr('font-family', this.model.get('font_family'));
498 this.update_attr('padding', this.model.get('padding'));
496 this.update_attr('padding', this.model.get('padding'));
499 this.update_attr('margin', this.model.get('margin'));
497 this.update_attr('margin', this.model.get('margin'));
500 this.update_attr('border-radius', this.model.get('border_radius'));
498 this.update_attr('border-radius', this.model.get('border_radius'));
501 }, this);
499 }, this);
502 },
500 },
503
501
504 _default_px: function(value) {
502 _default_px: function(value) {
505 // Makes browser interpret a numerical string as a pixel value.
503 // Makes browser interpret a numerical string as a pixel value.
506 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
504 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
507 return value.trim() + 'px';
505 return value.trim() + 'px';
508 }
506 }
509 return value;
507 return value;
510 },
508 },
511
509
512 update_attr: function(name, value) {
510 update_attr: function(name, value) {
513 // Set a css attr of the widget view.
511 // Set a css attr of the widget view.
514 this.$el.css(name, value);
512 this.$el.css(name, value);
515 },
513 },
516
514
517 update_visible: function(model, value) {
515 update_visible: function(model, value) {
518 // Update visibility
516 // Update visibility
519 this.$el.toggle(value);
517 this.$el.toggle(value);
520 },
518 },
521
519
522 update_css: function (model, css) {
520 update_css: function (model, css) {
523 // Update the css styling of this view.
521 // Update the css styling of this view.
524 var e = this.$el;
522 var e = this.$el;
525 if (css === undefined) {return;}
523 if (css === undefined) {return;}
526 for (var i = 0; i < css.length; i++) {
524 for (var i = 0; i < css.length; i++) {
527 // Apply the css traits to all elements that match the selector.
525 // Apply the css traits to all elements that match the selector.
528 var selector = css[i][0];
526 var selector = css[i][0];
529 var elements = this._get_selector_element(selector);
527 var elements = this._get_selector_element(selector);
530 if (elements.length > 0) {
528 if (elements.length > 0) {
531 var trait_key = css[i][1];
529 var trait_key = css[i][1];
532 var trait_value = css[i][2];
530 var trait_value = css[i][2];
533 elements.css(trait_key ,trait_value);
531 elements.css(trait_key ,trait_value);
534 }
532 }
535 }
533 }
536 },
534 },
537
535
538 update_classes: function (old_classes, new_classes, $el) {
536 update_classes: function (old_classes, new_classes, $el) {
539 // Update the DOM classes applied to an element, default to this.$el.
537 // Update the DOM classes applied to an element, default to this.$el.
540 if ($el===undefined) {
538 if ($el===undefined) {
541 $el = this.$el;
539 $el = this.$el;
542 }
540 }
543 this.do_diff(old_classes, new_classes, function(removed) {
541 this.do_diff(old_classes, new_classes, function(removed) {
544 $el.removeClass(removed);
542 $el.removeClass(removed);
545 }, function(added) {
543 }, function(added) {
546 $el.addClass(added);
544 $el.addClass(added);
547 });
545 });
548 },
546 },
549
547
550 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
548 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
549 // Update the DOM classes applied to the widget based on a single
552 // trait's value.
550 // trait's value.
553 //
551 //
554 // Given a trait value classes map, this function automatically
552 // Given a trait value classes map, this function automatically
555 // handles applying the appropriate classes to the widget element
553 // handles applying the appropriate classes to the widget element
556 // and removing classes that are no longer valid.
554 // and removing classes that are no longer valid.
557 //
555 //
558 // Parameters
556 // Parameters
559 // ----------
557 // ----------
560 // class_map: dictionary
558 // class_map: dictionary
561 // Dictionary of trait values to class lists.
559 // Dictionary of trait values to class lists.
562 // Example:
560 // Example:
563 // {
561 // {
564 // success: ['alert', 'alert-success'],
562 // success: ['alert', 'alert-success'],
565 // info: ['alert', 'alert-info'],
563 // info: ['alert', 'alert-info'],
566 // warning: ['alert', 'alert-warning'],
564 // warning: ['alert', 'alert-warning'],
567 // danger: ['alert', 'alert-danger']
565 // danger: ['alert', 'alert-danger']
568 // };
566 // };
569 // trait_name: string
567 // trait_name: string
570 // Name of the trait to check the value of.
568 // Name of the trait to check the value of.
571 // previous_trait_value: optional string, default ''
569 // previous_trait_value: optional string, default ''
572 // Last trait value
570 // Last trait value
573 // $el: optional jQuery element handle, defaults to this.$el
571 // $el: optional jQuery element handle, defaults to this.$el
574 // Element that the classes are applied to.
572 // Element that the classes are applied to.
575 var key = previous_trait_value;
573 var key = previous_trait_value;
576 if (key === undefined) {
574 if (key === undefined) {
577 key = this.model.previous(trait_name);
575 key = this.model.previous(trait_name);
578 }
576 }
579 var old_classes = class_map[key] ? class_map[key] : [];
577 var old_classes = class_map[key] ? class_map[key] : [];
580 key = this.model.get(trait_name);
578 key = this.model.get(trait_name);
581 var new_classes = class_map[key] ? class_map[key] : [];
579 var new_classes = class_map[key] ? class_map[key] : [];
582
580
583 this.update_classes(old_classes, new_classes, $el || this.$el);
581 this.update_classes(old_classes, new_classes, $el || this.$el);
584 },
582 },
585
583
586 _get_selector_element: function (selector) {
584 _get_selector_element: function (selector) {
587 // Get the elements via the css selector.
585 // Get the elements via the css selector.
588 var elements;
586 var elements;
589 if (!selector) {
587 if (!selector) {
590 elements = this.$el;
588 elements = this.$el;
591 } else {
589 } else {
592 elements = this.$el.find(selector).addBack(selector);
590 elements = this.$el.find(selector).addBack(selector);
593 }
591 }
594 return elements;
592 return elements;
595 },
593 },
596 });
594 });
597
595
598 widgetmanager.WidgetManager.register_widget_view('DOMWidgetView', DOMWidgetView);
599
596
600 var widget = {
597 var widget = {
601 'WidgetModel': WidgetModel,
598 'WidgetModel': WidgetModel,
602 'WidgetView': WidgetView,
599 'WidgetView': WidgetView,
603 'DOMWidgetView': DOMWidgetView,
600 'DOMWidgetView': DOMWidgetView,
604 };
601 };
605
602
606 // For backwards compatability.
603 // For backwards compatability.
607 $.extend(IPython, widget);
604 $.extend(IPython, widget);
608
605
609 return widget;
606 return widget;
610 });
607 });
@@ -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('WidgetView', 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
211
212 def send(self, content):
212 def send(self, content):
213 """Sends a custom msg to the widget model in the front-end.
213 """Sends a custom msg to the widget model in the front-end.
214
214
215 Parameters
215 Parameters
216 ----------
216 ----------
217 content : dict
217 content : dict
218 Content of the message to send.
218 Content of the message to send.
219 """
219 """
220 self._send({"method": "custom", "content": content})
220 self._send({"method": "custom", "content": content})
221
221
222 def on_msg(self, callback, remove=False):
222 def on_msg(self, callback, remove=False):
223 """(Un)Register a custom msg receive callback.
223 """(Un)Register a custom msg receive callback.
224
224
225 Parameters
225 Parameters
226 ----------
226 ----------
227 callback: callable
227 callback: callable
228 callback will be passed two arguments when a message arrives::
228 callback will be passed two arguments when a message arrives::
229
229
230 callback(widget, content)
230 callback(widget, content)
231
231
232 remove: bool
232 remove: bool
233 True if the callback should be unregistered."""
233 True if the callback should be unregistered."""
234 self._msg_callbacks.register_callback(callback, remove=remove)
234 self._msg_callbacks.register_callback(callback, remove=remove)
235
235
236 def on_displayed(self, callback, remove=False):
236 def on_displayed(self, callback, remove=False):
237 """(Un)Register a widget displayed callback.
237 """(Un)Register a widget displayed callback.
238
238
239 Parameters
239 Parameters
240 ----------
240 ----------
241 callback: method handler
241 callback: method handler
242 Must have a signature of::
242 Must have a signature of::
243
243
244 callback(widget, **kwargs)
244 callback(widget, **kwargs)
245
245
246 kwargs from display are passed through without modification.
246 kwargs from display are passed through without modification.
247 remove: bool
247 remove: bool
248 True if the callback should be unregistered."""
248 True if the callback should be unregistered."""
249 self._display_callbacks.register_callback(callback, remove=remove)
249 self._display_callbacks.register_callback(callback, remove=remove)
250
250
251 #-------------------------------------------------------------------------
251 #-------------------------------------------------------------------------
252 # Support methods
252 # Support methods
253 #-------------------------------------------------------------------------
253 #-------------------------------------------------------------------------
254 @contextmanager
254 @contextmanager
255 def _lock_property(self, key, value):
255 def _lock_property(self, key, value):
256 """Lock a property-value pair.
256 """Lock a property-value pair.
257
257
258 The value should be the JSON state of the property.
258 The value should be the JSON state of the property.
259
259
260 NOTE: This, in addition to the single lock for all state changes, is
260 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
261 flawed. In the future we may want to look into buffering state changes
262 back to the front-end."""
262 back to the front-end."""
263 self._property_lock = (key, value)
263 self._property_lock = (key, value)
264 try:
264 try:
265 yield
265 yield
266 finally:
266 finally:
267 self._property_lock = (None, None)
267 self._property_lock = (None, None)
268
268
269 @contextmanager
269 @contextmanager
270 def hold_sync(self):
270 def hold_sync(self):
271 """Hold syncing any state until the context manager is released"""
271 """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
272 # We increment a value so that this can be nested. Syncing will happen when
273 # all levels have been released.
273 # all levels have been released.
274 self._send_state_lock += 1
274 self._send_state_lock += 1
275 try:
275 try:
276 yield
276 yield
277 finally:
277 finally:
278 self._send_state_lock -=1
278 self._send_state_lock -=1
279 if self._send_state_lock == 0:
279 if self._send_state_lock == 0:
280 self.send_state(self._states_to_send)
280 self.send_state(self._states_to_send)
281 self._states_to_send.clear()
281 self._states_to_send.clear()
282
282
283 def _should_send_property(self, key, value):
283 def _should_send_property(self, key, value):
284 """Check the property lock (property_lock)"""
284 """Check the property lock (property_lock)"""
285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
286 if (key == self._property_lock[0]
286 if (key == self._property_lock[0]
287 and to_json(value) == self._property_lock[1]):
287 and to_json(value) == self._property_lock[1]):
288 return False
288 return False
289 elif self._send_state_lock > 0:
289 elif self._send_state_lock > 0:
290 self._states_to_send.add(key)
290 self._states_to_send.add(key)
291 return False
291 return False
292 else:
292 else:
293 return True
293 return True
294
294
295 # Event handlers
295 # Event handlers
296 @_show_traceback
296 @_show_traceback
297 def _handle_msg(self, msg):
297 def _handle_msg(self, msg):
298 """Called when a msg is received from the front-end"""
298 """Called when a msg is received from the front-end"""
299 data = msg['content']['data']
299 data = msg['content']['data']
300 method = data['method']
300 method = data['method']
301 if not method in ['backbone', 'custom']:
301 if not method in ['backbone', 'custom']:
302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
303
303
304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
305 if method == 'backbone' and 'sync_data' in data:
305 if method == 'backbone' and 'sync_data' in data:
306 sync_data = data['sync_data']
306 sync_data = data['sync_data']
307 self._handle_receive_state(sync_data) # handles all methods
307 self._handle_receive_state(sync_data) # handles all methods
308
308
309 # Handle a custom msg from the front-end
309 # Handle a custom msg from the front-end
310 elif method == 'custom':
310 elif method == 'custom':
311 if 'content' in data:
311 if 'content' in data:
312 self._handle_custom_msg(data['content'])
312 self._handle_custom_msg(data['content'])
313
313
314 def _handle_receive_state(self, sync_data):
314 def _handle_receive_state(self, sync_data):
315 """Called when a state is received from the front-end."""
315 """Called when a state is received from the front-end."""
316 for name in self.keys:
316 for name in self.keys:
317 if name in sync_data:
317 if name in sync_data:
318 json_value = sync_data[name]
318 json_value = sync_data[name]
319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
320 with self._lock_property(name, json_value):
320 with self._lock_property(name, json_value):
321 setattr(self, name, from_json(json_value))
321 setattr(self, name, from_json(json_value))
322
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. By sending a display message, the comm is opened and the
371 # Show view.
372 # initial state is sent.
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