##// END OF EJS Templates
Merge pull request #7757 from jasongrout/custom-serialization...
Jonathan Frederic -
r21035:d6e249b0 merge
parent child Browse files
Show More
@@ -1,34 +1,34 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([
4 define([
5 "widgets/js/manager",
5 "widgets/js/manager",
6 "widgets/js/widget",
6 "widgets/js/widget_link",
7 "widgets/js/widget_link",
7 "widgets/js/widget_bool",
8 "widgets/js/widget_bool",
8 "widgets/js/widget_button",
9 "widgets/js/widget_button",
9 "widgets/js/widget_box",
10 "widgets/js/widget_box",
10 "widgets/js/widget_float",
11 "widgets/js/widget_float",
11 "widgets/js/widget_image",
12 "widgets/js/widget_image",
12 "widgets/js/widget_int",
13 "widgets/js/widget_int",
13 "widgets/js/widget_output",
14 "widgets/js/widget_output",
14 "widgets/js/widget_selection",
15 "widgets/js/widget_selection",
15 "widgets/js/widget_selectioncontainer",
16 "widgets/js/widget_selectioncontainer",
16 "widgets/js/widget_string",
17 "widgets/js/widget_string",
17 ], function(widgetmanager, linkModels) {
18 ], function(widgetmanager, widget) {
18 for (var target_name in linkModels) {
19 // Register all of the loaded models and views with the widget manager.
19 if (linkModels.hasOwnProperty(target_name)) {
20 widgetmanager.WidgetManager.register_widget_model(target_name, linkModels[target_name]);
21 }
22 }
23
24 // Register all of the loaded views with the widget manager.
25 for (var i = 2; i < arguments.length; i++) {
20 for (var i = 2; i < arguments.length; i++) {
26 for (var target_name in arguments[i]) {
21 var module = arguments[i];
27 if (arguments[i].hasOwnProperty(target_name)) {
22 for (var target_name in module) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, arguments[i][target_name]);
23 if (module.hasOwnProperty(target_name)) {
24 var target = module[target_name];
25 if (target.prototype instanceof widget.WidgetModel) {
26 widgetmanager.WidgetManager.register_widget_model(target_name, target);
27 } else if (target.prototype instanceof widget.WidgetView) {
28 widgetmanager.WidgetManager.register_widget_view(target_name, target);
29 }
29 }
30 }
30 }
31 }
31 }
32 }
32
33 return {'WidgetManager': widgetmanager.WidgetManager};
33 return {'WidgetManager': widgetmanager.WidgetManager};
34 });
34 });
@@ -1,763 +1,780 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/utils",
8 "base/js/utils",
9 "base/js/namespace",
9 "base/js/namespace",
10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 "use strict";
11 "use strict";
12
12
13 var WidgetModel = Backbone.Model.extend({
13 var WidgetModel = Backbone.Model.extend({
14 constructor: function (widget_manager, model_id, comm) {
14 constructor: function (widget_manager, model_id, comm) {
15 /**
15 /**
16 * Constructor
16 * Constructor
17 *
17 *
18 * Creates a WidgetModel instance.
18 * Creates a WidgetModel instance.
19 *
19 *
20 * Parameters
20 * Parameters
21 * ----------
21 * ----------
22 * widget_manager : WidgetManager instance
22 * widget_manager : WidgetManager instance
23 * model_id : string
23 * model_id : string
24 * An ID unique to this model.
24 * An ID unique to this model.
25 * comm : Comm instance (optional)
25 * comm : Comm instance (optional)
26 */
26 */
27 this.widget_manager = widget_manager;
27 this.widget_manager = widget_manager;
28 this.state_change = Promise.resolve();
28 this.state_change = Promise.resolve();
29 this._buffered_state_diff = {};
29 this._buffered_state_diff = {};
30 this.pending_msgs = 0;
30 this.pending_msgs = 0;
31 this.msg_buffer = null;
31 this.msg_buffer = null;
32 this.state_lock = null;
32 this.state_lock = null;
33 this.id = model_id;
33 this.id = model_id;
34 this.views = {};
34 this.views = {};
35 this._resolve_received_state = {};
35 this._resolve_received_state = {};
36
36
37 if (comm !== undefined) {
37 if (comm !== undefined) {
38 // Remember comm associated with the model.
38 // Remember comm associated with the model.
39 this.comm = comm;
39 this.comm = comm;
40 comm.model = this;
40 comm.model = this;
41
41
42 // Hook comm messages up to model.
42 // Hook comm messages up to model.
43 comm.on_close($.proxy(this._handle_comm_closed, this));
43 comm.on_close($.proxy(this._handle_comm_closed, this));
44 comm.on_msg($.proxy(this._handle_comm_msg, this));
44 comm.on_msg($.proxy(this._handle_comm_msg, this));
45
45
46 // Assume the comm is alive.
46 // Assume the comm is alive.
47 this.set_comm_live(true);
47 this.set_comm_live(true);
48 } else {
48 } else {
49 this.set_comm_live(false);
49 this.set_comm_live(false);
50 }
50 }
51
51
52 // Listen for the events that lead to the websocket being terminated.
52 // Listen for the events that lead to the websocket being terminated.
53 var that = this;
53 var that = this;
54 var died = function() {
54 var died = function() {
55 that.set_comm_live(false);
55 that.set_comm_live(false);
56 };
56 };
57 widget_manager.notebook.events.on('kernel_disconnected.Kernel', died);
57 widget_manager.notebook.events.on('kernel_disconnected.Kernel', died);
58 widget_manager.notebook.events.on('kernel_killed.Kernel', died);
58 widget_manager.notebook.events.on('kernel_killed.Kernel', died);
59 widget_manager.notebook.events.on('kernel_restarting.Kernel', died);
59 widget_manager.notebook.events.on('kernel_restarting.Kernel', died);
60 widget_manager.notebook.events.on('kernel_dead.Kernel', died);
60 widget_manager.notebook.events.on('kernel_dead.Kernel', died);
61
61
62 return Backbone.Model.apply(this);
62 return Backbone.Model.apply(this);
63 },
63 },
64
64
65 send: function (content, callbacks) {
65 send: function (content, callbacks, buffers) {
66 /**
66 /**
67 * Send a custom msg over the comm.
67 * Send a custom msg over the comm.
68 */
68 */
69 if (this.comm !== undefined) {
69 if (this.comm !== undefined) {
70 var data = {method: 'custom', content: content};
70 var data = {method: 'custom', content: content};
71 this.comm.send(data, callbacks);
71 this.comm.send(data, callbacks, {}, buffers);
72 this.pending_msgs++;
72 this.pending_msgs++;
73 }
73 }
74 },
74 },
75
75
76 request_state: function(callbacks) {
76 request_state: function(callbacks) {
77 /**
77 /**
78 * Request a state push from the back-end.
78 * Request a state push from the back-end.
79 */
79 */
80 if (!this.comm) {
80 if (!this.comm) {
81 console.error("Could not request_state because comm doesn't exist!");
81 console.error("Could not request_state because comm doesn't exist!");
82 return;
82 return;
83 }
83 }
84
84
85 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
85 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
86
86
87 // Promise that is resolved when a state is received
87 // Promise that is resolved when a state is received
88 // from the back-end.
88 // from the back-end.
89 var that = this;
89 var that = this;
90 var received_state = new Promise(function(resolve) {
90 var received_state = new Promise(function(resolve) {
91 that._resolve_received_state[msg_id] = resolve;
91 that._resolve_received_state[msg_id] = resolve;
92 });
92 });
93 return received_state;
93 return received_state;
94 },
94 },
95
95
96 set_comm_live: function(live) {
96 set_comm_live: function(live) {
97 /**
97 /**
98 * Change the comm_live state of the model.
98 * Change the comm_live state of the model.
99 */
99 */
100 if (this.comm_live === undefined || this.comm_live != live) {
100 if (this.comm_live === undefined || this.comm_live != live) {
101 this.comm_live = live;
101 this.comm_live = live;
102 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
102 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
103 }
103 }
104 },
104 },
105
105
106 close: function(comm_closed) {
106 close: function(comm_closed) {
107 /**
107 /**
108 * Close model
108 * Close model
109 */
109 */
110 if (this.comm && !comm_closed) {
110 if (this.comm && !comm_closed) {
111 this.comm.close();
111 this.comm.close();
112 }
112 }
113 this.stopListening();
113 this.stopListening();
114 this.trigger('destroy', this);
114 this.trigger('destroy', this);
115 delete this.comm.model; // Delete ref so GC will collect widget model.
115 delete this.comm.model; // Delete ref so GC will collect widget model.
116 delete this.comm;
116 delete this.comm;
117 delete this.model_id; // Delete id from model so widget manager cleans up.
117 delete this.model_id; // Delete id from model so widget manager cleans up.
118 _.each(this.views, function(v, id, views) {
118 _.each(this.views, function(v, id, views) {
119 v.then(function(view) {
119 v.then(function(view) {
120 view.remove();
120 view.remove();
121 delete views[id];
121 delete views[id];
122 });
122 });
123 });
123 });
124 },
124 },
125
125
126 _handle_comm_closed: function (msg) {
126 _handle_comm_closed: function (msg) {
127 /**
127 /**
128 * Handle when a widget is closed.
128 * Handle when a widget is closed.
129 */
129 */
130 this.trigger('comm:close');
130 this.trigger('comm:close');
131 this.close(true);
131 this.close(true);
132 },
132 },
133
133
134 _handle_comm_msg: function (msg) {
134 _handle_comm_msg: function (msg) {
135 /**
135 /**
136 * Handle incoming comm msg.
136 * Handle incoming comm msg.
137 */
137 */
138 var method = msg.content.data.method;
138 var method = msg.content.data.method;
139
139 var that = this;
140 var that = this;
140 switch (method) {
141 switch (method) {
141 case 'update':
142 case 'update':
142 this.state_change = this.state_change
143 this.state_change = this.state_change
143 .then(function() {
144 .then(function() {
144 return that.set_state(msg.content.data.state);
145 var state = msg.content.data.state || {};
146 var buffer_keys = msg.content.data.buffers || [];
147 var buffers = msg.buffers || [];
148 for (var i=0; i<buffer_keys.length; i++) {
149 state[buffer_keys[i]] = buffers[i];
150 }
151
152 // deserialize fields that have custom deserializers
153 var serializers = that.constructor.serializers;
154 if (serializers) {
155 for (var k in state) {
156 if (serializers[k] && serializers[k].deserialize) {
157 state[k] = (serializers[k].deserialize)(state[k], that);
158 }
159 }
160 }
161 return utils.resolve_promises_dict(state);
162 }).then(function(state) {
163 return that.set_state(state);
145 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
164 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
146 .then(function() {
165 .then(function() {
147 var parent_id = msg.parent_header.msg_id;
166 var parent_id = msg.parent_header.msg_id;
148 if (that._resolve_received_state[parent_id] !== undefined) {
167 if (that._resolve_received_state[parent_id] !== undefined) {
149 that._resolve_received_state[parent_id].call();
168 that._resolve_received_state[parent_id].call();
150 delete that._resolve_received_state[parent_id];
169 delete that._resolve_received_state[parent_id];
151 }
170 }
152 }).catch(utils.reject("Couldn't resolve state request promise", true));
171 }).catch(utils.reject("Couldn't resolve state request promise", true));
153 break;
172 break;
154 case 'custom':
173 case 'custom':
155 this.trigger('msg:custom', msg.content.data.content);
174 this.trigger('msg:custom', msg.content.data.content, msg.buffers);
156 break;
175 break;
157 case 'display':
176 case 'display':
158 this.state_change = this.state_change.then(function() {
177 this.state_change = this.state_change.then(function() {
159 that.widget_manager.display_view(msg, that);
178 that.widget_manager.display_view(msg, that);
160 }).catch(utils.reject('Could not process display view msg', true));
179 }).catch(utils.reject('Could not process display view msg', true));
161 break;
180 break;
162 }
181 }
163 },
182 },
164
183
165 set_state: function (state) {
184 set_state: function (state) {
166 var that = this;
185 var that = this;
167 // Handle when a widget is updated via the python side.
186 // Handle when a widget is updated via the python side.
168 return this._unpack_models(state).then(function(state) {
187 return new Promise(function(resolve, reject) {
169 that.state_lock = state;
188 that.state_lock = state;
170 try {
189 try {
171 WidgetModel.__super__.set.call(that, state);
190 WidgetModel.__super__.set.call(that, state);
172 } finally {
191 } finally {
173 that.state_lock = null;
192 that.state_lock = null;
174 }
193 }
194 resolve();
175 }).catch(utils.reject("Couldn't set model state", true));
195 }).catch(utils.reject("Couldn't set model state", true));
176 },
196 },
177
197
178 get_state: function() {
198 get_state: function() {
179 // Get the serializable state of the model.
199 // Get the serializable state of the model.
180 var state = this.toJSON();
200 // Equivalent to Backbone.Model.toJSON()
181 for (var key in state) {
201 return _.clone(this.attributes);
182 if (state.hasOwnProperty(key)) {
183 state[key] = this._pack_models(state[key]);
184 }
185 }
186 return state;
187 },
202 },
188
203
189 _handle_status: function (msg, callbacks) {
204 _handle_status: function (msg, callbacks) {
190 /**
205 /**
191 * Handle status msgs.
206 * Handle status msgs.
192 *
207 *
193 * execution_state : ('busy', 'idle', 'starting')
208 * execution_state : ('busy', 'idle', 'starting')
194 */
209 */
195 if (this.comm !== undefined) {
210 if (this.comm !== undefined) {
196 if (msg.content.execution_state ==='idle') {
211 if (msg.content.execution_state ==='idle') {
197 // Send buffer if this message caused another message to be
212 // Send buffer if this message caused another message to be
198 // throttled.
213 // throttled.
199 if (this.msg_buffer !== null &&
214 if (this.msg_buffer !== null &&
200 (this.get('msg_throttle') || 3) === this.pending_msgs) {
215 (this.get('msg_throttle') || 3) === this.pending_msgs) {
201 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
216 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
202 this.comm.send(data, callbacks);
217 this.comm.send(data, callbacks);
203 this.msg_buffer = null;
218 this.msg_buffer = null;
204 } else {
219 } else {
205 --this.pending_msgs;
220 --this.pending_msgs;
206 }
221 }
207 }
222 }
208 }
223 }
209 },
224 },
210
225
211 callbacks: function(view) {
226 callbacks: function(view) {
212 /**
227 /**
213 * Create msg callbacks for a comm msg.
228 * Create msg callbacks for a comm msg.
214 */
229 */
215 var callbacks = this.widget_manager.callbacks(view);
230 var callbacks = this.widget_manager.callbacks(view);
216
231
217 if (callbacks.iopub === undefined) {
232 if (callbacks.iopub === undefined) {
218 callbacks.iopub = {};
233 callbacks.iopub = {};
219 }
234 }
220
235
221 var that = this;
236 var that = this;
222 callbacks.iopub.status = function (msg) {
237 callbacks.iopub.status = function (msg) {
223 that._handle_status(msg, callbacks);
238 that._handle_status(msg, callbacks);
224 };
239 };
225 return callbacks;
240 return callbacks;
226 },
241 },
227
242
228 set: function(key, val, options) {
243 set: function(key, val, options) {
229 /**
244 /**
230 * Set a value.
245 * Set a value.
231 */
246 */
232 var return_value = WidgetModel.__super__.set.apply(this, arguments);
247 var return_value = WidgetModel.__super__.set.apply(this, arguments);
233
248
234 // Backbone only remembers the diff of the most recent set()
249 // Backbone only remembers the diff of the most recent set()
235 // operation. Calling set multiple times in a row results in a
250 // operation. Calling set multiple times in a row results in a
236 // loss of diff information. Here we keep our own running diff.
251 // loss of diff information. Here we keep our own running diff.
237 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
252 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
238 return return_value;
253 return return_value;
239 },
254 },
240
255
241 sync: function (method, model, options) {
256 sync: function (method, model, options) {
242 /**
257 /**
243 * Handle sync to the back-end. Called when a model.save() is called.
258 * Handle sync to the back-end. Called when a model.save() is called.
244 *
259 *
245 * Make sure a comm exists.
260 * Make sure a comm exists.
261
262 * Parameters
263 * ----------
264 * method : create, update, patch, delete, read
265 * create/update always send the full attribute set
266 * patch - only send attributes listed in options.attrs, and if we are queuing
267 * up messages, combine with previous messages that have not been sent yet
268 * model : the model we are syncing
269 * will normally be the same as `this`
270 * options : dict
271 * the `attrs` key, if it exists, gives an {attr: value} dict that should be synced,
272 * otherwise, sync all attributes
273 *
246 */
274 */
247 var error = options.error || function() {
275 var error = options.error || function() {
248 console.error('Backbone sync error:', arguments);
276 console.error('Backbone sync error:', arguments);
249 };
277 };
250 if (this.comm === undefined) {
278 if (this.comm === undefined) {
251 error();
279 error();
252 return false;
280 return false;
253 }
281 }
254
282
255 // Delete any key value pairs that the back-end already knows about.
283 var attrs = (method === 'patch') ? options.attrs : model.get_state(options);
256 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
284
285 // the state_lock lists attributes that are currently be changed right now from a kernel message
286 // we don't want to send these non-changes back to the kernel, so we delete them out of attrs
287 // (but we only delete them if the value hasn't changed from the value stored in the state_lock
257 if (this.state_lock !== null) {
288 if (this.state_lock !== null) {
258 var keys = Object.keys(this.state_lock);
289 var keys = Object.keys(this.state_lock);
259 for (var i=0; i<keys.length; i++) {
290 for (var i=0; i<keys.length; i++) {
260 var key = keys[i];
291 var key = keys[i];
261 if (attrs[key] === this.state_lock[key]) {
292 if (attrs[key] === this.state_lock[key]) {
262 delete attrs[key];
293 delete attrs[key];
263 }
294 }
264 }
295 }
265 }
296 }
266
297
267 // Only sync if there are attributes to send to the back-end.
268 attrs = this._pack_models(attrs);
269 if (_.size(attrs) > 0) {
298 if (_.size(attrs) > 0) {
270
299
271 // If this message was sent via backbone itself, it will not
300 // If this message was sent via backbone itself, it will not
272 // have any callbacks. It's important that we create callbacks
301 // have any callbacks. It's important that we create callbacks
273 // so we can listen for status messages, etc...
302 // so we can listen for status messages, etc...
274 var callbacks = options.callbacks || this.callbacks();
303 var callbacks = options.callbacks || this.callbacks();
275
304
276 // Check throttle.
305 // Check throttle.
277 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
306 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
278 // The throttle has been exceeded, buffer the current msg so
307 // The throttle has been exceeded, buffer the current msg so
279 // it can be sent once the kernel has finished processing
308 // it can be sent once the kernel has finished processing
280 // some of the existing messages.
309 // some of the existing messages.
281
310
282 // Combine updates if it is a 'patch' sync, otherwise replace updates
311 // Combine updates if it is a 'patch' sync, otherwise replace updates
283 switch (method) {
312 switch (method) {
284 case 'patch':
313 case 'patch':
285 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
314 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
286 break;
315 break;
287 case 'update':
316 case 'update':
288 case 'create':
317 case 'create':
289 this.msg_buffer = attrs;
318 this.msg_buffer = attrs;
290 break;
319 break;
291 default:
320 default:
292 error();
321 error();
293 return false;
322 return false;
294 }
323 }
295 this.msg_buffer_callbacks = callbacks;
324 this.msg_buffer_callbacks = callbacks;
296
325
297 } else {
326 } else {
298 // We haven't exceeded the throttle, send the message like
327 // We haven't exceeded the throttle, send the message like
299 // normal.
328 // normal.
300 var data = {method: 'backbone', sync_data: attrs};
329 this.send_sync_message(attrs, callbacks);
301 this.comm.send(data, callbacks);
302 this.pending_msgs++;
330 this.pending_msgs++;
303 }
331 }
304 }
332 }
305 // Since the comm is a one-way communication, assume the message
333 // Since the comm is a one-way communication, assume the message
306 // arrived. Don't call success since we don't have a model back from the server
334 // arrived. Don't call success since we don't have a model back from the server
307 // this means we miss out on the 'sync' event.
335 // this means we miss out on the 'sync' event.
308 this._buffered_state_diff = {};
336 this._buffered_state_diff = {};
309 },
337 },
310
338
339
340 send_sync_message: function(attrs, callbacks) {
341 // prepare and send a comm message syncing attrs
342 var that = this;
343 // first, build a state dictionary with key=the attribute and the value
344 // being the value or the promise of the serialized value
345 var serializers = this.constructor.serializers;
346 if (serializers) {
347 for (k in attrs) {
348 if (serializers[k] && serializers[k].serialize) {
349 attrs[k] = (serializers[k].serialize)(attrs[k], this);
350 }
351 }
352 }
353 utils.resolve_promises_dict(attrs).then(function(state) {
354 // get binary values, then send
355 var keys = Object.keys(state);
356 var buffers = [];
357 var buffer_keys = [];
358 for (var i=0; i<keys.length; i++) {
359 var key = keys[i];
360 var value = state[key];
361 if (value.buffer instanceof ArrayBuffer
362 || value instanceof ArrayBuffer) {
363 buffers.push(value);
364 buffer_keys.push(key);
365 delete state[key];
366 }
367 }
368 that.comm.send({method: 'backbone', sync_data: state, buffer_keys: buffer_keys}, callbacks, {}, buffers);
369 }).catch(function(error) {
370 that.pending_msgs--;
371 return (utils.reject("Couldn't send widget sync message", true))(error);
372 });
373 },
374
311 save_changes: function(callbacks) {
375 save_changes: function(callbacks) {
312 /**
376 /**
313 * Push this model's state to the back-end
377 * Push this model's state to the back-end
314 *
378 *
315 * This invokes a Backbone.Sync.
379 * This invokes a Backbone.Sync.
316 */
380 */
317 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
381 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
318 },
382 },
319
383
320 _pack_models: function(value) {
321 /**
322 * Replace models with model ids recursively.
323 */
324 var that = this;
325 var packed;
326 if (value instanceof Backbone.Model) {
327 return "IPY_MODEL_" + value.id;
328
329 } else if ($.isArray(value)) {
330 packed = [];
331 _.each(value, function(sub_value, key) {
332 packed.push(that._pack_models(sub_value));
333 });
334 return packed;
335 } else if (value instanceof Date || value instanceof String) {
336 return value;
337 } else if (value instanceof Object) {
338 packed = {};
339 _.each(value, function(sub_value, key) {
340 packed[key] = that._pack_models(sub_value);
341 });
342 return packed;
343
344 } else {
345 return value;
346 }
347 },
348
349 _unpack_models: function(value) {
350 /**
351 * Replace model ids with models recursively.
352 */
353 var that = this;
354 var unpacked;
355 if ($.isArray(value)) {
356 unpacked = [];
357 _.each(value, function(sub_value, key) {
358 unpacked.push(that._unpack_models(sub_value));
359 });
360 return Promise.all(unpacked);
361 } else if (value instanceof Object) {
362 unpacked = {};
363 _.each(value, function(sub_value, key) {
364 unpacked[key] = that._unpack_models(sub_value);
365 });
366 return utils.resolve_promises_dict(unpacked);
367 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
368 // get_model returns a promise already
369 return this.widget_manager.get_model(value.slice(10, value.length));
370 } else {
371 return Promise.resolve(value);
372 }
373 },
374
375 on_some_change: function(keys, callback, context) {
384 on_some_change: function(keys, callback, context) {
376 /**
385 /**
377 * on_some_change(["key1", "key2"], foo, context) differs from
386 * on_some_change(["key1", "key2"], foo, context) differs from
378 * on("change:key1 change:key2", foo, context).
387 * on("change:key1 change:key2", foo, context).
379 * If the widget attributes key1 and key2 are both modified,
388 * If the widget attributes key1 and key2 are both modified,
380 * the second form will result in foo being called twice
389 * the second form will result in foo being called twice
381 * while the first will call foo only once.
390 * while the first will call foo only once.
382 */
391 */
383 this.on('change', function() {
392 this.on('change', function() {
384 if (keys.some(this.hasChanged, this)) {
393 if (keys.some(this.hasChanged, this)) {
385 callback.apply(context);
394 callback.apply(context);
386 }
395 }
387 }, this);
396 }, this);
388
397
389 },
398 },
399
400 toJSON: function(options) {
401 /**
402 * Serialize the model. See the types.js deserialization function
403 * and the kernel-side serializer/deserializer
404 */
405 return "IPY_MODEL_"+this.id;
406 }
390 });
407 });
391 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
408 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
392
409
393
410
394 var WidgetView = Backbone.View.extend({
411 var WidgetView = Backbone.View.extend({
395 initialize: function(parameters) {
412 initialize: function(parameters) {
396 /**
413 /**
397 * Public constructor.
414 * Public constructor.
398 */
415 */
399 this.model.on('change',this.update,this);
416 this.model.on('change',this.update,this);
400
417
401 // Bubble the comm live events.
418 // Bubble the comm live events.
402 this.model.on('comm:live', function() {
419 this.model.on('comm:live', function() {
403 this.trigger('comm:live', this);
420 this.trigger('comm:live', this);
404 }, this);
421 }, this);
405 this.model.on('comm:dead', function() {
422 this.model.on('comm:dead', function() {
406 this.trigger('comm:dead', this);
423 this.trigger('comm:dead', this);
407 }, this);
424 }, this);
408
425
409 this.options = parameters.options;
426 this.options = parameters.options;
410 this.on('displayed', function() {
427 this.on('displayed', function() {
411 this.is_displayed = true;
428 this.is_displayed = true;
412 }, this);
429 }, this);
413 },
430 },
414
431
415 update: function(){
432 update: function(){
416 /**
433 /**
417 * Triggered on model change.
434 * Triggered on model change.
418 *
435 *
419 * Update view to be consistent with this.model
436 * Update view to be consistent with this.model
420 */
437 */
421 },
438 },
422
439
423 create_child_view: function(child_model, options) {
440 create_child_view: function(child_model, options) {
424 /**
441 /**
425 * Create and promise that resolves to a child view of a given model
442 * Create and promise that resolves to a child view of a given model
426 */
443 */
427 var that = this;
444 var that = this;
428 options = $.extend({ parent: this }, options || {});
445 options = $.extend({ parent: this }, options || {});
429 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
446 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view", true));
430 },
447 },
431
448
432 callbacks: function(){
449 callbacks: function(){
433 /**
450 /**
434 * Create msg callbacks for a comm msg.
451 * Create msg callbacks for a comm msg.
435 */
452 */
436 return this.model.callbacks(this);
453 return this.model.callbacks(this);
437 },
454 },
438
455
439 render: function(){
456 render: function(){
440 /**
457 /**
441 * Render the view.
458 * Render the view.
442 *
459 *
443 * By default, this is only called the first time the view is created
460 * By default, this is only called the first time the view is created
444 */
461 */
445 },
462 },
446
463
447 send: function (content) {
464 send: function (content, buffers) {
448 /**
465 /**
449 * Send a custom msg associated with this view.
466 * Send a custom msg associated with this view.
450 */
467 */
451 this.model.send(content, this.callbacks());
468 this.model.send(content, this.callbacks(), buffers);
452 },
469 },
453
470
454 touch: function () {
471 touch: function () {
455 this.model.save_changes(this.callbacks());
472 this.model.save_changes(this.callbacks());
456 },
473 },
457
474
458 after_displayed: function (callback, context) {
475 after_displayed: function (callback, context) {
459 /**
476 /**
460 * Calls the callback right away is the view is already displayed
477 * Calls the callback right away is the view is already displayed
461 * otherwise, register the callback to the 'displayed' event.
478 * otherwise, register the callback to the 'displayed' event.
462 */
479 */
463 if (this.is_displayed) {
480 if (this.is_displayed) {
464 callback.apply(context);
481 callback.apply(context);
465 } else {
482 } else {
466 this.on('displayed', callback, context);
483 this.on('displayed', callback, context);
467 }
484 }
468 },
485 },
469
486
470 remove: function () {
487 remove: function () {
471 // Raise a remove event when the view is removed.
488 // Raise a remove event when the view is removed.
472 WidgetView.__super__.remove.apply(this, arguments);
489 WidgetView.__super__.remove.apply(this, arguments);
473 this.trigger('remove');
490 this.trigger('remove');
474 }
491 }
475 });
492 });
476
493
477
494
478 var DOMWidgetView = WidgetView.extend({
495 var DOMWidgetView = WidgetView.extend({
479 initialize: function (parameters) {
496 initialize: function (parameters) {
480 /**
497 /**
481 * Public constructor
498 * Public constructor
482 */
499 */
483 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
500 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
484 this.model.on('change:visible', this.update_visible, this);
501 this.model.on('change:visible', this.update_visible, this);
485 this.model.on('change:_css', this.update_css, this);
502 this.model.on('change:_css', this.update_css, this);
486
503
487 this.model.on('change:_dom_classes', function(model, new_classes) {
504 this.model.on('change:_dom_classes', function(model, new_classes) {
488 var old_classes = model.previous('_dom_classes');
505 var old_classes = model.previous('_dom_classes');
489 this.update_classes(old_classes, new_classes);
506 this.update_classes(old_classes, new_classes);
490 }, this);
507 }, this);
491
508
492 this.model.on('change:color', function (model, value) {
509 this.model.on('change:color', function (model, value) {
493 this.update_attr('color', value); }, this);
510 this.update_attr('color', value); }, this);
494
511
495 this.model.on('change:background_color', function (model, value) {
512 this.model.on('change:background_color', function (model, value) {
496 this.update_attr('background', value); }, this);
513 this.update_attr('background', value); }, this);
497
514
498 this.model.on('change:width', function (model, value) {
515 this.model.on('change:width', function (model, value) {
499 this.update_attr('width', value); }, this);
516 this.update_attr('width', value); }, this);
500
517
501 this.model.on('change:height', function (model, value) {
518 this.model.on('change:height', function (model, value) {
502 this.update_attr('height', value); }, this);
519 this.update_attr('height', value); }, this);
503
520
504 this.model.on('change:border_color', function (model, value) {
521 this.model.on('change:border_color', function (model, value) {
505 this.update_attr('border-color', value); }, this);
522 this.update_attr('border-color', value); }, this);
506
523
507 this.model.on('change:border_width', function (model, value) {
524 this.model.on('change:border_width', function (model, value) {
508 this.update_attr('border-width', value); }, this);
525 this.update_attr('border-width', value); }, this);
509
526
510 this.model.on('change:border_style', function (model, value) {
527 this.model.on('change:border_style', function (model, value) {
511 this.update_attr('border-style', value); }, this);
528 this.update_attr('border-style', value); }, this);
512
529
513 this.model.on('change:font_style', function (model, value) {
530 this.model.on('change:font_style', function (model, value) {
514 this.update_attr('font-style', value); }, this);
531 this.update_attr('font-style', value); }, this);
515
532
516 this.model.on('change:font_weight', function (model, value) {
533 this.model.on('change:font_weight', function (model, value) {
517 this.update_attr('font-weight', value); }, this);
534 this.update_attr('font-weight', value); }, this);
518
535
519 this.model.on('change:font_size', function (model, value) {
536 this.model.on('change:font_size', function (model, value) {
520 this.update_attr('font-size', this._default_px(value)); }, this);
537 this.update_attr('font-size', this._default_px(value)); }, this);
521
538
522 this.model.on('change:font_family', function (model, value) {
539 this.model.on('change:font_family', function (model, value) {
523 this.update_attr('font-family', value); }, this);
540 this.update_attr('font-family', value); }, this);
524
541
525 this.model.on('change:padding', function (model, value) {
542 this.model.on('change:padding', function (model, value) {
526 this.update_attr('padding', value); }, this);
543 this.update_attr('padding', value); }, this);
527
544
528 this.model.on('change:margin', function (model, value) {
545 this.model.on('change:margin', function (model, value) {
529 this.update_attr('margin', this._default_px(value)); }, this);
546 this.update_attr('margin', this._default_px(value)); }, this);
530
547
531 this.model.on('change:border_radius', function (model, value) {
548 this.model.on('change:border_radius', function (model, value) {
532 this.update_attr('border-radius', this._default_px(value)); }, this);
549 this.update_attr('border-radius', this._default_px(value)); }, this);
533
550
534 this.after_displayed(function() {
551 this.after_displayed(function() {
535 this.update_visible(this.model, this.model.get("visible"));
552 this.update_visible(this.model, this.model.get("visible"));
536 this.update_classes([], this.model.get('_dom_classes'));
553 this.update_classes([], this.model.get('_dom_classes'));
537
554
538 this.update_attr('color', this.model.get('color'));
555 this.update_attr('color', this.model.get('color'));
539 this.update_attr('background', this.model.get('background_color'));
556 this.update_attr('background', this.model.get('background_color'));
540 this.update_attr('width', this.model.get('width'));
557 this.update_attr('width', this.model.get('width'));
541 this.update_attr('height', this.model.get('height'));
558 this.update_attr('height', this.model.get('height'));
542 this.update_attr('border-color', this.model.get('border_color'));
559 this.update_attr('border-color', this.model.get('border_color'));
543 this.update_attr('border-width', this.model.get('border_width'));
560 this.update_attr('border-width', this.model.get('border_width'));
544 this.update_attr('border-style', this.model.get('border_style'));
561 this.update_attr('border-style', this.model.get('border_style'));
545 this.update_attr('font-style', this.model.get('font_style'));
562 this.update_attr('font-style', this.model.get('font_style'));
546 this.update_attr('font-weight', this.model.get('font_weight'));
563 this.update_attr('font-weight', this.model.get('font_weight'));
547 this.update_attr('font-size', this._default_px(this.model.get('font_size')));
564 this.update_attr('font-size', this._default_px(this.model.get('font_size')));
548 this.update_attr('font-family', this.model.get('font_family'));
565 this.update_attr('font-family', this.model.get('font_family'));
549 this.update_attr('padding', this.model.get('padding'));
566 this.update_attr('padding', this.model.get('padding'));
550 this.update_attr('margin', this._default_px(this.model.get('margin')));
567 this.update_attr('margin', this._default_px(this.model.get('margin')));
551 this.update_attr('border-radius', this._default_px(this.model.get('border_radius')));
568 this.update_attr('border-radius', this._default_px(this.model.get('border_radius')));
552
569
553 this.update_css(this.model, this.model.get("_css"));
570 this.update_css(this.model, this.model.get("_css"));
554 }, this);
571 }, this);
555 },
572 },
556
573
557 _default_px: function(value) {
574 _default_px: function(value) {
558 /**
575 /**
559 * Makes browser interpret a numerical string as a pixel value.
576 * Makes browser interpret a numerical string as a pixel value.
560 */
577 */
561 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
578 if (value && /^\d+\.?(\d+)?$/.test(value.trim())) {
562 return value.trim() + 'px';
579 return value.trim() + 'px';
563 }
580 }
564 return value;
581 return value;
565 },
582 },
566
583
567 update_attr: function(name, value) {
584 update_attr: function(name, value) {
568 /**
585 /**
569 * Set a css attr of the widget view.
586 * Set a css attr of the widget view.
570 */
587 */
571 this.$el.css(name, value);
588 this.$el.css(name, value);
572 },
589 },
573
590
574 update_visible: function(model, value) {
591 update_visible: function(model, value) {
575 /**
592 /**
576 * Update visibility
593 * Update visibility
577 */
594 */
578 switch(value) {
595 switch(value) {
579 case null: // python None
596 case null: // python None
580 this.$el.show().css('visibility', 'hidden'); break;
597 this.$el.show().css('visibility', 'hidden'); break;
581 case false:
598 case false:
582 this.$el.hide(); break;
599 this.$el.hide(); break;
583 case true:
600 case true:
584 this.$el.show().css('visibility', ''); break;
601 this.$el.show().css('visibility', ''); break;
585 }
602 }
586 },
603 },
587
604
588 update_css: function (model, css) {
605 update_css: function (model, css) {
589 /**
606 /**
590 * Update the css styling of this view.
607 * Update the css styling of this view.
591 */
608 */
592 if (css === undefined) {return;}
609 if (css === undefined) {return;}
593 for (var i = 0; i < css.length; i++) {
610 for (var i = 0; i < css.length; i++) {
594 // Apply the css traits to all elements that match the selector.
611 // Apply the css traits to all elements that match the selector.
595 var selector = css[i][0];
612 var selector = css[i][0];
596 var elements = this._get_selector_element(selector);
613 var elements = this._get_selector_element(selector);
597 if (elements.length > 0) {
614 if (elements.length > 0) {
598 var trait_key = css[i][1];
615 var trait_key = css[i][1];
599 var trait_value = css[i][2];
616 var trait_value = css[i][2];
600 elements.css(trait_key ,trait_value);
617 elements.css(trait_key ,trait_value);
601 }
618 }
602 }
619 }
603 },
620 },
604
621
605 update_classes: function (old_classes, new_classes, $el) {
622 update_classes: function (old_classes, new_classes, $el) {
606 /**
623 /**
607 * Update the DOM classes applied to an element, default to this.$el.
624 * Update the DOM classes applied to an element, default to this.$el.
608 */
625 */
609 if ($el===undefined) {
626 if ($el===undefined) {
610 $el = this.$el;
627 $el = this.$el;
611 }
628 }
612 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
629 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
613 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
630 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
614 },
631 },
615
632
616 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
633 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
617 /**
634 /**
618 * Update the DOM classes applied to the widget based on a single
635 * Update the DOM classes applied to the widget based on a single
619 * trait's value.
636 * trait's value.
620 *
637 *
621 * Given a trait value classes map, this function automatically
638 * Given a trait value classes map, this function automatically
622 * handles applying the appropriate classes to the widget element
639 * handles applying the appropriate classes to the widget element
623 * and removing classes that are no longer valid.
640 * and removing classes that are no longer valid.
624 *
641 *
625 * Parameters
642 * Parameters
626 * ----------
643 * ----------
627 * class_map: dictionary
644 * class_map: dictionary
628 * Dictionary of trait values to class lists.
645 * Dictionary of trait values to class lists.
629 * Example:
646 * Example:
630 * {
647 * {
631 * success: ['alert', 'alert-success'],
648 * success: ['alert', 'alert-success'],
632 * info: ['alert', 'alert-info'],
649 * info: ['alert', 'alert-info'],
633 * warning: ['alert', 'alert-warning'],
650 * warning: ['alert', 'alert-warning'],
634 * danger: ['alert', 'alert-danger']
651 * danger: ['alert', 'alert-danger']
635 * };
652 * };
636 * trait_name: string
653 * trait_name: string
637 * Name of the trait to check the value of.
654 * Name of the trait to check the value of.
638 * previous_trait_value: optional string, default ''
655 * previous_trait_value: optional string, default ''
639 * Last trait value
656 * Last trait value
640 * $el: optional jQuery element handle, defaults to this.$el
657 * $el: optional jQuery element handle, defaults to this.$el
641 * Element that the classes are applied to.
658 * Element that the classes are applied to.
642 */
659 */
643 var key = previous_trait_value;
660 var key = previous_trait_value;
644 if (key === undefined) {
661 if (key === undefined) {
645 key = this.model.previous(trait_name);
662 key = this.model.previous(trait_name);
646 }
663 }
647 var old_classes = class_map[key] ? class_map[key] : [];
664 var old_classes = class_map[key] ? class_map[key] : [];
648 key = this.model.get(trait_name);
665 key = this.model.get(trait_name);
649 var new_classes = class_map[key] ? class_map[key] : [];
666 var new_classes = class_map[key] ? class_map[key] : [];
650
667
651 this.update_classes(old_classes, new_classes, $el || this.$el);
668 this.update_classes(old_classes, new_classes, $el || this.$el);
652 },
669 },
653
670
654 _get_selector_element: function (selector) {
671 _get_selector_element: function (selector) {
655 /**
672 /**
656 * Get the elements via the css selector.
673 * Get the elements via the css selector.
657 */
674 */
658 var elements;
675 var elements;
659 if (!selector) {
676 if (!selector) {
660 elements = this.$el;
677 elements = this.$el;
661 } else {
678 } else {
662 elements = this.$el.find(selector).addBack(selector);
679 elements = this.$el.find(selector).addBack(selector);
663 }
680 }
664 return elements;
681 return elements;
665 },
682 },
666
683
667 typeset: function(element, text){
684 typeset: function(element, text){
668 utils.typeset.apply(null, arguments);
685 utils.typeset.apply(null, arguments);
669 },
686 },
670 });
687 });
671
688
672
689
673 var ViewList = function(create_view, remove_view, context) {
690 var ViewList = function(create_view, remove_view, context) {
674 /**
691 /**
675 * - create_view and remove_view are default functions called when adding or removing views
692 * - create_view and remove_view are default functions called when adding or removing views
676 * - create_view takes a model and returns a view or a promise for a view for that model
693 * - create_view takes a model and returns a view or a promise for a view for that model
677 * - remove_view takes a view and destroys it (including calling `view.remove()`)
694 * - remove_view takes a view and destroys it (including calling `view.remove()`)
678 * - each time the update() function is called with a new list, the create and remove
695 * - each time the update() function is called with a new list, the create and remove
679 * callbacks will be called in an order so that if you append the views created in the
696 * callbacks will be called in an order so that if you append the views created in the
680 * create callback and remove the views in the remove callback, you will duplicate
697 * create callback and remove the views in the remove callback, you will duplicate
681 * the order of the list.
698 * the order of the list.
682 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
699 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
683 * - the context defaults to the created ViewList. If you pass another context, the create and remove
700 * - the context defaults to the created ViewList. If you pass another context, the create and remove
684 * will be called in that context.
701 * will be called in that context.
685 */
702 */
686
703
687 this.initialize.apply(this, arguments);
704 this.initialize.apply(this, arguments);
688 };
705 };
689
706
690 _.extend(ViewList.prototype, {
707 _.extend(ViewList.prototype, {
691 initialize: function(create_view, remove_view, context) {
708 initialize: function(create_view, remove_view, context) {
692 this._handler_context = context || this;
709 this._handler_context = context || this;
693 this._models = [];
710 this._models = [];
694 this.views = []; // list of promises for views
711 this.views = []; // list of promises for views
695 this._create_view = create_view;
712 this._create_view = create_view;
696 this._remove_view = remove_view || function(view) {view.remove();};
713 this._remove_view = remove_view || function(view) {view.remove();};
697 },
714 },
698
715
699 update: function(new_models, create_view, remove_view, context) {
716 update: function(new_models, create_view, remove_view, context) {
700 /**
717 /**
701 * the create_view, remove_view, and context arguments override the defaults
718 * the create_view, remove_view, and context arguments override the defaults
702 * specified when the list is created.
719 * specified when the list is created.
703 * after this function, the .views attribute is a list of promises for views
720 * after this function, the .views attribute is a list of promises for views
704 * if you want to perform some action on the list of views, do something like
721 * if you want to perform some action on the list of views, do something like
705 * `Promise.all(myviewlist.views).then(function(views) {...});`
722 * `Promise.all(myviewlist.views).then(function(views) {...});`
706 */
723 */
707 var remove = remove_view || this._remove_view;
724 var remove = remove_view || this._remove_view;
708 var create = create_view || this._create_view;
725 var create = create_view || this._create_view;
709 context = context || this._handler_context;
726 context = context || this._handler_context;
710 var i = 0;
727 var i = 0;
711 // first, skip past the beginning of the lists if they are identical
728 // first, skip past the beginning of the lists if they are identical
712 for (; i < new_models.length; i++) {
729 for (; i < new_models.length; i++) {
713 if (i >= this._models.length || new_models[i] !== this._models[i]) {
730 if (i >= this._models.length || new_models[i] !== this._models[i]) {
714 break;
731 break;
715 }
732 }
716 }
733 }
717
734
718 var first_removed = i;
735 var first_removed = i;
719 // Remove the non-matching items from the old list.
736 // Remove the non-matching items from the old list.
720 var removed = this.views.splice(first_removed, this.views.length-first_removed);
737 var removed = this.views.splice(first_removed, this.views.length-first_removed);
721 for (var j = 0; j < removed.length; j++) {
738 for (var j = 0; j < removed.length; j++) {
722 removed[j].then(function(view) {
739 removed[j].then(function(view) {
723 remove.call(context, view)
740 remove.call(context, view)
724 });
741 });
725 }
742 }
726
743
727 // Add the rest of the new list items.
744 // Add the rest of the new list items.
728 for (; i < new_models.length; i++) {
745 for (; i < new_models.length; i++) {
729 this.views.push(Promise.resolve(create.call(context, new_models[i])));
746 this.views.push(Promise.resolve(create.call(context, new_models[i])));
730 }
747 }
731 // make a copy of the input array
748 // make a copy of the input array
732 this._models = new_models.slice();
749 this._models = new_models.slice();
733 },
750 },
734
751
735 remove: function() {
752 remove: function() {
736 /**
753 /**
737 * removes every view in the list; convenience function for `.update([])`
754 * removes every view in the list; convenience function for `.update([])`
738 * that should be faster
755 * that should be faster
739 * returns a promise that resolves after this removal is done
756 * returns a promise that resolves after this removal is done
740 */
757 */
741 var that = this;
758 var that = this;
742 return Promise.all(this.views).then(function(views) {
759 return Promise.all(this.views).then(function(views) {
743 for (var i = 0; i < that.views.length; i++) {
760 for (var i = 0; i < that.views.length; i++) {
744 that._remove_view.call(that._handler_context, views[i]);
761 that._remove_view.call(that._handler_context, views[i]);
745 }
762 }
746 that.views = [];
763 that.views = [];
747 that._models = [];
764 that._models = [];
748 });
765 });
749 },
766 },
750 });
767 });
751
768
752 var widget = {
769 var widget = {
753 'WidgetModel': WidgetModel,
770 'WidgetModel': WidgetModel,
754 'WidgetView': WidgetView,
771 'WidgetView': WidgetView,
755 'DOMWidgetView': DOMWidgetView,
772 'DOMWidgetView': DOMWidgetView,
756 'ViewList': ViewList,
773 'ViewList': ViewList,
757 };
774 };
758
775
759 // For backwards compatability.
776 // For backwards compatability.
760 $.extend(IPython, widget);
777 $.extend(IPython, widget);
761
778
762 return widget;
779 return widget;
763 });
780 });
@@ -1,154 +1,187 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([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "underscore",
7 "base/js/utils",
8 "base/js/utils",
8 "bootstrap",
9 "bootstrap",
9 ], function(widget, $, utils){
10 ], function(widget, $, _, utils){
10 "use strict";
11 "use strict";
12 var unpack_models = function unpack_models(value, model) {
13 /**
14 * Replace model ids with models recursively.
15 */
16 var unpacked;
17 if ($.isArray(value)) {
18 unpacked = [];
19 _.each(value, function(sub_value, key) {
20 unpacked.push(unpack_models(sub_value, model));
21 });
22 return Promise.all(unpacked);
23 } else if (value instanceof Object) {
24 unpacked = {};
25 _.each(value, function(sub_value, key) {
26 unpacked[key] = unpack_models(sub_value, model);
27 });
28 return utils.resolve_promises_dict(unpacked);
29 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
30 // get_model returns a promise already
31 return model.widget_manager.get_model(value.slice(10, value.length));
32 } else {
33 return Promise.resolve(value);
34 }
35 };
36
37 var BoxModel = widget.WidgetModel.extend({}, {
38 serializers: _.extend({
39 children: {deserialize: unpack_models}
40 }, widget.WidgetModel.serializers)
41 });
11
42
12 var BoxView = widget.DOMWidgetView.extend({
43 var BoxView = widget.DOMWidgetView.extend({
13 initialize: function(){
44 initialize: function(){
14 /**
45 /**
15 * Public constructor
46 * Public constructor
16 */
47 */
17 BoxView.__super__.initialize.apply(this, arguments);
48 BoxView.__super__.initialize.apply(this, arguments);
18 this.children_views = new widget.ViewList(this.add_child_model, null, this);
49 this.children_views = new widget.ViewList(this.add_child_model, null, this);
19 this.listenTo(this.model, 'change:children', function(model, value) {
50 this.listenTo(this.model, 'change:children', function(model, value) {
20 this.children_views.update(value);
51 this.children_views.update(value);
21 }, this);
52 }, this);
22 this.listenTo(this.model, 'change:overflow_x', function(model, value) {
53 this.listenTo(this.model, 'change:overflow_x', function(model, value) {
23 this.update_overflow_x();
54 this.update_overflow_x();
24 }, this);
55 }, this);
25 this.listenTo(this.model, 'change:overflow_y', function(model, value) {
56 this.listenTo(this.model, 'change:overflow_y', function(model, value) {
26 this.update_overflow_y();
57 this.update_overflow_y();
27 }, this);
58 }, this);
28 this.listenTo(this.model, 'change:box_style', function(model, value) {
59 this.listenTo(this.model, 'change:box_style', function(model, value) {
29 this.update_box_style();
60 this.update_box_style();
30 }, this);
61 }, this);
31 },
62 },
32
63
33 update_attr: function(name, value) {
64 update_attr: function(name, value) {
34 /**
65 /**
35 * Set a css attr of the widget view.
66 * Set a css attr of the widget view.
36 */
67 */
37 this.$box.css(name, value);
68 this.$box.css(name, value);
38 },
69 },
39
70
40 render: function(){
71 render: function(){
41 /**
72 /**
42 * Called when view is rendered.
73 * Called when view is rendered.
43 */
74 */
44 this.$box = this.$el;
75 this.$box = this.$el;
45 this.$box.addClass('widget-box');
76 this.$box.addClass('widget-box');
46 this.children_views.update(this.model.get('children'));
77 this.children_views.update(this.model.get('children'));
47 this.update_overflow_x();
78 this.update_overflow_x();
48 this.update_overflow_y();
79 this.update_overflow_y();
49 this.update_box_style('');
80 this.update_box_style('');
50 },
81 },
51
82
52 update_overflow_x: function() {
83 update_overflow_x: function() {
53 /**
84 /**
54 * Called when the x-axis overflow setting is changed.
85 * Called when the x-axis overflow setting is changed.
55 */
86 */
56 this.$box.css('overflow-x', this.model.get('overflow_x'));
87 this.$box.css('overflow-x', this.model.get('overflow_x'));
57 },
88 },
58
89
59 update_overflow_y: function() {
90 update_overflow_y: function() {
60 /**
91 /**
61 * Called when the y-axis overflow setting is changed.
92 * Called when the y-axis overflow setting is changed.
62 */
93 */
63 this.$box.css('overflow-y', this.model.get('overflow_y'));
94 this.$box.css('overflow-y', this.model.get('overflow_y'));
64 },
95 },
65
96
66 update_box_style: function(previous_trait_value) {
97 update_box_style: function(previous_trait_value) {
67 var class_map = {
98 var class_map = {
68 success: ['alert', 'alert-success'],
99 success: ['alert', 'alert-success'],
69 info: ['alert', 'alert-info'],
100 info: ['alert', 'alert-info'],
70 warning: ['alert', 'alert-warning'],
101 warning: ['alert', 'alert-warning'],
71 danger: ['alert', 'alert-danger']
102 danger: ['alert', 'alert-danger']
72 };
103 };
73 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
104 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
74 },
105 },
75
106
76 add_child_model: function(model) {
107 add_child_model: function(model) {
77 /**
108 /**
78 * Called when a model is added to the children list.
109 * Called when a model is added to the children list.
79 */
110 */
80 var that = this;
111 var that = this;
81 var dummy = $('<div/>');
112 var dummy = $('<div/>');
82 that.$box.append(dummy);
113 that.$box.append(dummy);
83 return this.create_child_view(model).then(function(view) {
114 return this.create_child_view(model).then(function(view) {
84 dummy.replaceWith(view.el);
115 dummy.replaceWith(view.el);
85
116
86 // Trigger the displayed event of the child view.
117 // Trigger the displayed event of the child view.
87 that.after_displayed(function() {
118 that.after_displayed(function() {
88 view.trigger('displayed');
119 view.trigger('displayed');
89 });
120 });
90 return view;
121 return view;
91 }).catch(utils.reject("Couldn't add child view to box", true));
122 }).catch(utils.reject("Couldn't add child view to box", true));
92 },
123 },
93
124
94 remove: function() {
125 remove: function() {
95 /**
126 /**
96 * We remove this widget before removing the children as an optimization
127 * We remove this widget before removing the children as an optimization
97 * we want to remove the entire container from the DOM first before
128 * we want to remove the entire container from the DOM first before
98 * removing each individual child separately.
129 * removing each individual child separately.
99 */
130 */
100 BoxView.__super__.remove.apply(this, arguments);
131 BoxView.__super__.remove.apply(this, arguments);
101 this.children_views.remove();
132 this.children_views.remove();
102 },
133 },
103 });
134 });
104
135
105
136
106 var FlexBoxView = BoxView.extend({
137 var FlexBoxView = BoxView.extend({
107 render: function(){
138 render: function(){
108 FlexBoxView.__super__.render.apply(this);
139 FlexBoxView.__super__.render.apply(this);
109 this.listenTo(this.model, 'change:orientation', this.update_orientation, this);
140 this.listenTo(this.model, 'change:orientation', this.update_orientation, this);
110 this.listenTo(this.model, 'change:flex', this._flex_changed, this);
141 this.listenTo(this.model, 'change:flex', this._flex_changed, this);
111 this.listenTo(this.model, 'change:pack', this._pack_changed, this);
142 this.listenTo(this.model, 'change:pack', this._pack_changed, this);
112 this.listenTo(this.model, 'change:align', this._align_changed, this);
143 this.listenTo(this.model, 'change:align', this._align_changed, this);
113 this._flex_changed();
144 this._flex_changed();
114 this._pack_changed();
145 this._pack_changed();
115 this._align_changed();
146 this._align_changed();
116 this.update_orientation();
147 this.update_orientation();
117 },
148 },
118
149
119 update_orientation: function(){
150 update_orientation: function(){
120 var orientation = this.model.get("orientation");
151 var orientation = this.model.get("orientation");
121 if (orientation == "vertical") {
152 if (orientation == "vertical") {
122 this.$box.removeClass("hbox").addClass("vbox");
153 this.$box.removeClass("hbox").addClass("vbox");
123 } else {
154 } else {
124 this.$box.removeClass("vbox").addClass("hbox");
155 this.$box.removeClass("vbox").addClass("hbox");
125 }
156 }
126 },
157 },
127
158
128 _flex_changed: function(){
159 _flex_changed: function(){
129 if (this.model.previous('flex')) {
160 if (this.model.previous('flex')) {
130 this.$box.removeClass('box-flex' + this.model.previous('flex'));
161 this.$box.removeClass('box-flex' + this.model.previous('flex'));
131 }
162 }
132 this.$box.addClass('box-flex' + this.model.get('flex'));
163 this.$box.addClass('box-flex' + this.model.get('flex'));
133 },
164 },
134
165
135 _pack_changed: function(){
166 _pack_changed: function(){
136 if (this.model.previous('pack')) {
167 if (this.model.previous('pack')) {
137 this.$box.removeClass(this.model.previous('pack'));
168 this.$box.removeClass(this.model.previous('pack'));
138 }
169 }
139 this.$box.addClass(this.model.get('pack'));
170 this.$box.addClass(this.model.get('pack'));
140 },
171 },
141
172
142 _align_changed: function(){
173 _align_changed: function(){
143 if (this.model.previous('align')) {
174 if (this.model.previous('align')) {
144 this.$box.removeClass('align-' + this.model.previous('align'));
175 this.$box.removeClass('align-' + this.model.previous('align'));
145 }
176 }
146 this.$box.addClass('align-' + this.model.get('align'));
177 this.$box.addClass('align-' + this.model.get('align'));
147 },
178 },
148 });
179 });
149
180
150 return {
181 return {
182 'unpack_models': unpack_models,
183 'BoxModel': BoxModel,
151 'BoxView': BoxView,
184 'BoxView': BoxView,
152 'FlexBoxView': FlexBoxView,
185 'FlexBoxView': FlexBoxView,
153 };
186 };
154 });
187 });
@@ -1,832 +1,845 b''
1 //
1 //
2 // Utility functions for the HTML notebook's CasperJS tests.
2 // Utility functions for the HTML notebook's CasperJS tests.
3 //
3 //
4 casper.get_notebook_server = function () {
4 casper.get_notebook_server = function () {
5 // Get the URL of a notebook server on which to run tests.
5 // Get the URL of a notebook server on which to run tests.
6 var port = casper.cli.get("port");
6 var port = casper.cli.get("port");
7 port = (typeof port === 'undefined') ? '8888' : port;
7 port = (typeof port === 'undefined') ? '8888' : port;
8 return casper.cli.get("url") || ('http://127.0.0.1:' + port);
8 return casper.cli.get("url") || ('http://127.0.0.1:' + port);
9 };
9 };
10
10
11 casper.open_new_notebook = function () {
11 casper.open_new_notebook = function () {
12 // Create and open a new notebook.
12 // Create and open a new notebook.
13 var baseUrl = this.get_notebook_server();
13 var baseUrl = this.get_notebook_server();
14 this.start(baseUrl);
14 this.start(baseUrl);
15 this.waitFor(this.page_loaded);
15 this.waitFor(this.page_loaded);
16 this.waitForSelector('#kernel-python2 a, #kernel-python3 a');
16 this.waitForSelector('#kernel-python2 a, #kernel-python3 a');
17 this.thenClick('#kernel-python2 a, #kernel-python3 a');
17 this.thenClick('#kernel-python2 a, #kernel-python3 a');
18
18
19 this.waitForPopup('');
19 this.waitForPopup('');
20
20
21 this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');});
21 this.withPopup('', function () {this.waitForSelector('.CodeMirror-code');});
22 this.then(function () {
22 this.then(function () {
23 this.open(this.popups[0].url);
23 this.open(this.popups[0].url);
24 });
24 });
25 this.waitFor(this.page_loaded);
25 this.waitFor(this.page_loaded);
26
26
27 // Hook the log and error methods of the console, forcing them to
27 // Hook the log and error methods of the console, forcing them to
28 // serialize their arguments before printing. This allows the
28 // serialize their arguments before printing. This allows the
29 // Objects to cross into the phantom/slimer regime for display.
29 // Objects to cross into the phantom/slimer regime for display.
30 this.thenEvaluate(function(){
30 this.thenEvaluate(function(){
31 var serialize_arguments = function(f, context) {
31 var serialize_arguments = function(f, context) {
32 return function() {
32 return function() {
33 var pretty_arguments = [];
33 var pretty_arguments = [];
34 for (var i = 0; i < arguments.length; i++) {
34 for (var i = 0; i < arguments.length; i++) {
35 var value = arguments[i];
35 var value = arguments[i];
36 if (value instanceof Object) {
36 if (value instanceof Object) {
37 var name = value.name || 'Object';
37 var name = value.name || 'Object';
38 // Print a JSON string representation of the object.
38 // Print a JSON string representation of the object.
39 // If we don't do this, [Object object] gets printed
39 // If we don't do this, [Object object] gets printed
40 // by casper, which is useless. The long regular
40 // by casper, which is useless. The long regular
41 // expression reduces the verbosity of the JSON.
41 // expression reduces the verbosity of the JSON.
42 pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ')
42 pretty_arguments.push(name + ' {' + JSON.stringify(value, null, ' ')
43 .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n')
43 .replace(/(\s+)?({)?(\s+)?(}(\s+)?,?)?(\s+)?(\s+)?\n/g, '\n')
44 .replace(/\n(\s+)?\n/g, '\n'));
44 .replace(/\n(\s+)?\n/g, '\n'));
45 } else {
45 } else {
46 pretty_arguments.push(value);
46 pretty_arguments.push(value);
47 }
47 }
48 }
48 }
49 f.apply(context, pretty_arguments);
49 f.apply(context, pretty_arguments);
50 };
50 };
51 };
51 };
52 console.log = serialize_arguments(console.log, console);
52 console.log = serialize_arguments(console.log, console);
53 console.error = serialize_arguments(console.error, console);
53 console.error = serialize_arguments(console.error, console);
54 });
54 });
55
55
56 // Make sure the kernel has started
56 // Make sure the kernel has started
57 this.waitFor(this.kernel_running);
57 this.waitFor(this.kernel_running);
58 // track the IPython busy/idle state
58 // track the IPython busy/idle state
59 this.thenEvaluate(function () {
59 this.thenEvaluate(function () {
60 require(['base/js/namespace', 'base/js/events'], function (IPython, events) {
60 require(['base/js/namespace', 'base/js/events'], function (IPython, events) {
61
61
62 events.on('kernel_idle.Kernel',function () {
62 events.on('kernel_idle.Kernel',function () {
63 IPython._status = 'idle';
63 IPython._status = 'idle';
64 });
64 });
65 events.on('kernel_busy.Kernel',function () {
65 events.on('kernel_busy.Kernel',function () {
66 IPython._status = 'busy';
66 IPython._status = 'busy';
67 });
67 });
68 });
68 });
69 });
69 });
70
70
71 // Because of the asynchronous nature of SlimerJS (Gecko), we need to make
71 // Because of the asynchronous nature of SlimerJS (Gecko), we need to make
72 // sure the notebook has actually been loaded into the IPython namespace
72 // sure the notebook has actually been loaded into the IPython namespace
73 // before running any tests.
73 // before running any tests.
74 this.waitFor(function() {
74 this.waitFor(function() {
75 return this.evaluate(function () {
75 return this.evaluate(function () {
76 return IPython.notebook;
76 return IPython.notebook;
77 });
77 });
78 });
78 });
79 };
79 };
80
80
81 casper.page_loaded = function() {
81 casper.page_loaded = function() {
82 // Return whether or not the kernel is running.
82 // Return whether or not the kernel is running.
83 return this.evaluate(function() {
83 return this.evaluate(function() {
84 return typeof IPython !== "undefined" &&
84 return typeof IPython !== "undefined" &&
85 IPython.page !== undefined;
85 IPython.page !== undefined;
86 });
86 });
87 };
87 };
88
88
89 casper.kernel_running = function() {
89 casper.kernel_running = function() {
90 // Return whether or not the kernel is running.
90 // Return whether or not the kernel is running.
91 return this.evaluate(function() {
91 return this.evaluate(function() {
92 return IPython &&
92 return IPython &&
93 IPython.notebook &&
93 IPython.notebook &&
94 IPython.notebook.kernel &&
94 IPython.notebook.kernel &&
95 IPython.notebook.kernel.is_connected();
95 IPython.notebook.kernel.is_connected();
96 });
96 });
97 };
97 };
98
98
99 casper.kernel_disconnected = function() {
99 casper.kernel_disconnected = function() {
100 return this.evaluate(function() {
100 return this.evaluate(function() {
101 return IPython.notebook.kernel.is_fully_disconnected();
101 return IPython.notebook.kernel.is_fully_disconnected();
102 });
102 });
103 };
103 };
104
104
105 casper.wait_for_kernel_ready = function () {
105 casper.wait_for_kernel_ready = function () {
106 this.waitFor(this.kernel_running);
106 this.waitFor(this.kernel_running);
107 this.thenEvaluate(function () {
107 this.thenEvaluate(function () {
108 IPython._kernel_ready = false;
108 IPython._kernel_ready = false;
109 IPython.notebook.kernel.kernel_info(
109 IPython.notebook.kernel.kernel_info(
110 function () {
110 function () {
111 IPython._kernel_ready = true;
111 IPython._kernel_ready = true;
112 });
112 });
113 });
113 });
114 this.waitFor(function () {
114 this.waitFor(function () {
115 return this.evaluate(function () {
115 return this.evaluate(function () {
116 return IPython._kernel_ready;
116 return IPython._kernel_ready;
117 });
117 });
118 });
118 });
119 };
119 };
120
120
121 casper.shutdown_current_kernel = function () {
121 casper.shutdown_current_kernel = function () {
122 // Shut down the current notebook's kernel.
122 // Shut down the current notebook's kernel.
123 this.thenEvaluate(function() {
123 this.thenEvaluate(function() {
124 IPython.notebook.session.delete();
124 IPython.notebook.session.delete();
125 });
125 });
126 // We close the page right after this so we need to give it time to complete.
126 // We close the page right after this so we need to give it time to complete.
127 this.wait(1000);
127 this.wait(1000);
128 };
128 };
129
129
130 casper.delete_current_notebook = function () {
130 casper.delete_current_notebook = function () {
131 // Delete created notebook.
131 // Delete created notebook.
132
132
133 // For some unknown reason, this doesn't work?!?
133 // For some unknown reason, this doesn't work?!?
134 this.thenEvaluate(function() {
134 this.thenEvaluate(function() {
135 IPython.notebook.delete();
135 IPython.notebook.delete();
136 });
136 });
137 };
137 };
138
138
139 casper.wait_for_busy = function () {
139 casper.wait_for_busy = function () {
140 // Waits for the notebook to enter a busy state.
140 // Waits for the notebook to enter a busy state.
141 this.waitFor(function () {
141 this.waitFor(function () {
142 return this.evaluate(function () {
142 return this.evaluate(function () {
143 return IPython._status == 'busy';
143 return IPython._status == 'busy';
144 });
144 });
145 });
145 });
146 };
146 };
147
147
148 casper.wait_for_idle = function () {
148 casper.wait_for_idle = function () {
149 // Waits for the notebook to idle.
149 // Waits for the notebook to idle.
150 this.waitFor(function () {
150 this.waitFor(function () {
151 return this.evaluate(function () {
151 return this.evaluate(function () {
152 return IPython._status == 'idle';
152 return IPython._status == 'idle';
153 });
153 });
154 });
154 });
155 };
155 };
156
156
157 casper.wait_for_output = function (cell_num, out_num) {
157 casper.wait_for_output = function (cell_num, out_num) {
158 // wait for the nth output in a given cell
158 // wait for the nth output in a given cell
159 this.wait_for_idle();
159 this.wait_for_idle();
160 out_num = out_num || 0;
160 out_num = out_num || 0;
161 this.then(function() {
161 this.then(function() {
162 this.waitFor(function (c, o) {
162 this.waitFor(function (c, o) {
163 return this.evaluate(function get_output(c, o) {
163 return this.evaluate(function get_output(c, o) {
164 var cell = IPython.notebook.get_cell(c);
164 var cell = IPython.notebook.get_cell(c);
165 return cell.output_area.outputs.length > o;
165 return cell.output_area.outputs.length > o;
166 },
166 },
167 // pass parameter from the test suite js to the browser code js
167 // pass parameter from the test suite js to the browser code js
168 {c : cell_num, o : out_num});
168 {c : cell_num, o : out_num});
169 });
169 });
170 },
170 },
171 function then() { },
171 function then() { },
172 function timeout() {
172 function timeout() {
173 this.echo("wait_for_output timed out!");
173 this.echo("wait_for_output timed out!");
174 });
174 });
175 };
175 };
176
176
177 casper.wait_for_widget = function (widget_info) {
177 casper.wait_for_widget = function (widget_info) {
178 // wait for a widget msg que to reach 0
178 // wait for a widget msg que to reach 0
179 //
179 //
180 // Parameters
180 // Parameters
181 // ----------
181 // ----------
182 // widget_info : object
182 // widget_info : object
183 // Object which contains info related to the widget. The model_id property
183 // Object which contains info related to the widget. The model_id property
184 // is used to identify the widget.
184 // is used to identify the widget.
185
185
186 // Clear the results of a previous query, if they exist. Make sure a
186 // Clear the results of a previous query, if they exist. Make sure a
187 // dictionary exists to store the async results in.
187 // dictionary exists to store the async results in.
188 this.thenEvaluate(function(model_id) {
188 this.thenEvaluate(function(model_id) {
189 if (window.pending_msgs === undefined) {
189 if (window.pending_msgs === undefined) {
190 window.pending_msgs = {};
190 window.pending_msgs = {};
191 } else {
191 } else {
192 window.pending_msgs[model_id] = -1;
192 window.pending_msgs[model_id] = -1;
193 }
193 }
194 }, {model_id: widget_info.model_id});
194 }, {model_id: widget_info.model_id});
195
195
196 // Wait for the pending messages to be 0.
196 // Wait for the pending messages to be 0.
197 this.waitFor(function () {
197 this.waitFor(function () {
198 var pending = this.evaluate(function (model_id) {
198 var pending = this.evaluate(function (model_id) {
199
199
200 // Get the model. Once the model is had, store it's pending_msgs
200 // Get the model. Once the model is had, store it's pending_msgs
201 // count in the window's dictionary.
201 // count in the window's dictionary.
202 IPython.notebook.kernel.widget_manager.get_model(model_id)
202 IPython.notebook.kernel.widget_manager.get_model(model_id)
203 .then(function(model) {
203 .then(function(model) {
204 window.pending_msgs[model_id] = model.pending_msgs;
204 window.pending_msgs[model_id] = model.pending_msgs;
205 });
205 });
206
206
207 // Return the pending_msgs result.
207 // Return the pending_msgs result.
208 return window.pending_msgs[model_id];
208 return window.pending_msgs[model_id];
209 }, {model_id: widget_info.model_id});
209 }, {model_id: widget_info.model_id});
210
210
211 if (pending === 0) {
211 if (pending === 0) {
212 return true;
212 return true;
213 } else {
213 } else {
214 return false;
214 return false;
215 }
215 }
216 });
216 });
217 };
217 };
218
218
219 casper.get_output_cell = function (cell_num, out_num) {
219 casper.get_output_cell = function (cell_num, out_num) {
220 // return an output of a given cell
220 // return an output of a given cell
221 out_num = out_num || 0;
221 out_num = out_num || 0;
222 var result = casper.evaluate(function (c, o) {
222 var result = casper.evaluate(function (c, o) {
223 var cell = IPython.notebook.get_cell(c);
223 var cell = IPython.notebook.get_cell(c);
224 return cell.output_area.outputs[o];
224 return cell.output_area.outputs[o];
225 },
225 },
226 {c : cell_num, o : out_num});
226 {c : cell_num, o : out_num});
227 if (!result) {
227 if (!result) {
228 var num_outputs = casper.evaluate(function (c) {
228 var num_outputs = casper.evaluate(function (c) {
229 var cell = IPython.notebook.get_cell(c);
229 var cell = IPython.notebook.get_cell(c);
230 return cell.output_area.outputs.length;
230 return cell.output_area.outputs.length;
231 },
231 },
232 {c : cell_num});
232 {c : cell_num});
233 this.test.assertTrue(false,
233 this.test.assertTrue(false,
234 "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
234 "Cell " + cell_num + " has no output #" + out_num + " (" + num_outputs + " total)"
235 );
235 );
236 } else {
236 } else {
237 return result;
237 return result;
238 }
238 }
239 };
239 };
240
240
241 casper.get_cells_length = function () {
241 casper.get_cells_length = function () {
242 // return the number of cells in the notebook
242 // return the number of cells in the notebook
243 var result = casper.evaluate(function () {
243 var result = casper.evaluate(function () {
244 return IPython.notebook.get_cells().length;
244 return IPython.notebook.get_cells().length;
245 });
245 });
246 return result;
246 return result;
247 };
247 };
248
248
249 casper.set_cell_text = function(index, text){
249 casper.set_cell_text = function(index, text){
250 // Set the text content of a cell.
250 // Set the text content of a cell.
251 this.evaluate(function (index, text) {
251 this.evaluate(function (index, text) {
252 var cell = IPython.notebook.get_cell(index);
252 var cell = IPython.notebook.get_cell(index);
253 cell.set_text(text);
253 cell.set_text(text);
254 }, index, text);
254 }, index, text);
255 };
255 };
256
256
257 casper.get_cell_text = function(index){
257 casper.get_cell_text = function(index){
258 // Get the text content of a cell.
258 // Get the text content of a cell.
259 return this.evaluate(function (index) {
259 return this.evaluate(function (index) {
260 var cell = IPython.notebook.get_cell(index);
260 var cell = IPython.notebook.get_cell(index);
261 return cell.get_text();
261 return cell.get_text();
262 }, index);
262 }, index);
263 };
263 };
264
264
265 casper.insert_cell_at_bottom = function(cell_type){
265 casper.insert_cell_at_bottom = function(cell_type){
266 // Inserts a cell at the bottom of the notebook
266 // Inserts a cell at the bottom of the notebook
267 // Returns the new cell's index.
267 // Returns the new cell's index.
268 return this.evaluate(function (cell_type) {
268 return this.evaluate(function (cell_type) {
269 var cell = IPython.notebook.insert_cell_at_bottom(cell_type);
269 var cell = IPython.notebook.insert_cell_at_bottom(cell_type);
270 return IPython.notebook.find_cell_index(cell);
270 return IPython.notebook.find_cell_index(cell);
271 }, cell_type);
271 }, cell_type);
272 };
272 };
273
273
274 casper.append_cell = function(text, cell_type) {
274 casper.append_cell = function(text, cell_type) {
275 // Insert a cell at the bottom of the notebook and set the cells text.
275 // Insert a cell at the bottom of the notebook and set the cells text.
276 // Returns the new cell's index.
276 // Returns the new cell's index.
277 var index = this.insert_cell_at_bottom(cell_type);
277 var index = this.insert_cell_at_bottom(cell_type);
278 if (text !== undefined) {
278 if (text !== undefined) {
279 this.set_cell_text(index, text);
279 this.set_cell_text(index, text);
280 }
280 }
281 return index;
281 return index;
282 };
282 };
283
283
284 casper.execute_cell = function(index, expect_failure){
284 casper.execute_cell = function(index, expect_failure){
285 // Asynchronously executes a cell by index.
285 // Asynchronously executes a cell by index.
286 // Returns the cell's index.
286 // Returns the cell's index.
287
287
288 if (expect_failure === undefined) expect_failure = false;
288 if (expect_failure === undefined) expect_failure = false;
289 var that = this;
289 var that = this;
290 this.then(function(){
290 this.then(function(){
291 that.evaluate(function (index) {
291 that.evaluate(function (index) {
292 var cell = IPython.notebook.get_cell(index);
292 var cell = IPython.notebook.get_cell(index);
293 cell.execute();
293 cell.execute();
294 }, index);
294 }, index);
295 });
295 });
296 this.wait_for_idle();
296 this.wait_for_idle();
297
297
298 this.then(function () {
298 this.then(function () {
299 var error = that.evaluate(function (index) {
299 var error = that.evaluate(function (index) {
300 var cell = IPython.notebook.get_cell(index);
300 var cell = IPython.notebook.get_cell(index);
301 var outputs = cell.output_area.outputs;
301 var outputs = cell.output_area.outputs;
302 for (var i = 0; i < outputs.length; i++) {
302 for (var i = 0; i < outputs.length; i++) {
303 if (outputs[i].output_type == 'error') {
303 if (outputs[i].output_type == 'error') {
304 return outputs[i];
304 return outputs[i];
305 }
305 }
306 }
306 }
307 return false;
307 return false;
308 }, index);
308 }, index);
309 if (error === null) {
309 if (error === null) {
310 this.test.fail("Failed to check for error output");
310 this.test.fail("Failed to check for error output");
311 }
311 }
312 if (expect_failure && error === false) {
312 if (expect_failure && error === false) {
313 this.test.fail("Expected error while running cell");
313 this.test.fail("Expected error while running cell");
314 } else if (!expect_failure && error !== false) {
314 } else if (!expect_failure && error !== false) {
315 this.test.fail("Error running cell:\n" + error.traceback.join('\n'));
315 this.test.fail("Error running cell:\n" + error.traceback.join('\n'));
316 }
316 }
317 });
317 });
318 return index;
318 return index;
319 };
319 };
320
320
321 casper.execute_cell_then = function(index, then_callback, expect_failure) {
321 casper.execute_cell_then = function(index, then_callback, expect_failure) {
322 // Synchronously executes a cell by index.
322 // Synchronously executes a cell by index.
323 // Optionally accepts a then_callback parameter. then_callback will get called
323 // Optionally accepts a then_callback parameter. then_callback will get called
324 // when the cell has finished executing.
324 // when the cell has finished executing.
325 // Returns the cell's index.
325 // Returns the cell's index.
326 var return_val = this.execute_cell(index, expect_failure);
326 var return_val = this.execute_cell(index, expect_failure);
327
327
328 this.wait_for_idle();
328 this.wait_for_idle();
329
329
330 var that = this;
330 var that = this;
331 this.then(function(){
331 this.then(function(){
332 if (then_callback!==undefined) {
332 if (then_callback!==undefined) {
333 then_callback.apply(that, [index]);
333 then_callback.apply(that, [index]);
334 }
334 }
335 });
335 });
336
336
337 return return_val;
337 return return_val;
338 };
338 };
339
339
340 casper.append_cell_execute_then = function(text, then_callback, expect_failure) {
341 // Append a code cell and execute it, optionally calling a then_callback
342 var c = this.append_cell(text);
343 return this.execute_cell_then(c, then_callback, expect_failure);
344 };
345
346 casper.assert_output_equals = function(text, output_text, message) {
347 // Append a code cell with the text, then assert the output is equal to output_text
348 this.append_cell_execute_then(text, function(index) {
349 this.test.assertEquals(this.get_output_cell(index).text.trim(), output_text, message);
350 });
351 };
352
340 casper.wait_for_element = function(index, selector){
353 casper.wait_for_element = function(index, selector){
341 // Utility function that allows us to easily wait for an element
354 // Utility function that allows us to easily wait for an element
342 // within a cell. Uses JQuery selector to look for the element.
355 // within a cell. Uses JQuery selector to look for the element.
343 var that = this;
356 var that = this;
344 this.waitFor(function() {
357 this.waitFor(function() {
345 return that.cell_element_exists(index, selector);
358 return that.cell_element_exists(index, selector);
346 });
359 });
347 };
360 };
348
361
349 casper.cell_element_exists = function(index, selector){
362 casper.cell_element_exists = function(index, selector){
350 // Utility function that allows us to easily check if an element exists
363 // Utility function that allows us to easily check if an element exists
351 // within a cell. Uses JQuery selector to look for the element.
364 // within a cell. Uses JQuery selector to look for the element.
352 return casper.evaluate(function (index, selector) {
365 return casper.evaluate(function (index, selector) {
353 var $cell = IPython.notebook.get_cell(index).element;
366 var $cell = IPython.notebook.get_cell(index).element;
354 return $cell.find(selector).length > 0;
367 return $cell.find(selector).length > 0;
355 }, index, selector);
368 }, index, selector);
356 };
369 };
357
370
358 casper.cell_element_function = function(index, selector, function_name, function_args){
371 casper.cell_element_function = function(index, selector, function_name, function_args){
359 // Utility function that allows us to execute a jQuery function on an
372 // Utility function that allows us to execute a jQuery function on an
360 // element within a cell.
373 // element within a cell.
361 return casper.evaluate(function (index, selector, function_name, function_args) {
374 return casper.evaluate(function (index, selector, function_name, function_args) {
362 var $cell = IPython.notebook.get_cell(index).element;
375 var $cell = IPython.notebook.get_cell(index).element;
363 var $el = $cell.find(selector);
376 var $el = $cell.find(selector);
364 return $el[function_name].apply($el, function_args);
377 return $el[function_name].apply($el, function_args);
365 }, index, selector, function_name, function_args);
378 }, index, selector, function_name, function_args);
366 };
379 };
367
380
368 casper.validate_notebook_state = function(message, mode, cell_index) {
381 casper.validate_notebook_state = function(message, mode, cell_index) {
369 // Validate the entire dual mode state of the notebook. Make sure no more than
382 // Validate the entire dual mode state of the notebook. Make sure no more than
370 // one cell is selected, focused, in edit mode, etc...
383 // one cell is selected, focused, in edit mode, etc...
371
384
372 // General tests.
385 // General tests.
373 this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
386 this.test.assertEquals(this.get_keyboard_mode(), this.get_notebook_mode(),
374 message + '; keyboard and notebook modes match');
387 message + '; keyboard and notebook modes match');
375 // Is the selected cell the only cell that is selected?
388 // Is the selected cell the only cell that is selected?
376 if (cell_index!==undefined) {
389 if (cell_index!==undefined) {
377 this.test.assert(this.is_only_cell_selected(cell_index),
390 this.test.assert(this.is_only_cell_selected(cell_index),
378 message + '; cell ' + cell_index + ' is the only cell selected');
391 message + '; cell ' + cell_index + ' is the only cell selected');
379 }
392 }
380
393
381 // Mode specific tests.
394 // Mode specific tests.
382 if (mode==='command') {
395 if (mode==='command') {
383 // Are the notebook and keyboard manager in command mode?
396 // Are the notebook and keyboard manager in command mode?
384 this.test.assertEquals(this.get_keyboard_mode(), 'command',
397 this.test.assertEquals(this.get_keyboard_mode(), 'command',
385 message + '; in command mode');
398 message + '; in command mode');
386 // Make sure there isn't a single cell in edit mode.
399 // Make sure there isn't a single cell in edit mode.
387 this.test.assert(this.is_only_cell_edit(null),
400 this.test.assert(this.is_only_cell_edit(null),
388 message + '; all cells in command mode');
401 message + '; all cells in command mode');
389 this.test.assert(this.is_cell_editor_focused(null),
402 this.test.assert(this.is_cell_editor_focused(null),
390 message + '; no cell editors are focused while in command mode');
403 message + '; no cell editors are focused while in command mode');
391
404
392 } else if (mode==='edit') {
405 } else if (mode==='edit') {
393 // Are the notebook and keyboard manager in edit mode?
406 // Are the notebook and keyboard manager in edit mode?
394 this.test.assertEquals(this.get_keyboard_mode(), 'edit',
407 this.test.assertEquals(this.get_keyboard_mode(), 'edit',
395 message + '; in edit mode');
408 message + '; in edit mode');
396 if (cell_index!==undefined) {
409 if (cell_index!==undefined) {
397 // Is the specified cell the only cell in edit mode?
410 // Is the specified cell the only cell in edit mode?
398 this.test.assert(this.is_only_cell_edit(cell_index),
411 this.test.assert(this.is_only_cell_edit(cell_index),
399 message + '; cell ' + cell_index + ' is the only cell in edit mode '+ this.cells_modes());
412 message + '; cell ' + cell_index + ' is the only cell in edit mode '+ this.cells_modes());
400 // Is the specified cell the only cell with a focused code mirror?
413 // Is the specified cell the only cell with a focused code mirror?
401 this.test.assert(this.is_cell_editor_focused(cell_index),
414 this.test.assert(this.is_cell_editor_focused(cell_index),
402 message + '; cell ' + cell_index + '\'s editor is appropriately focused');
415 message + '; cell ' + cell_index + '\'s editor is appropriately focused');
403 }
416 }
404
417
405 } else {
418 } else {
406 this.test.assert(false, message + '; ' + mode + ' is an unknown mode');
419 this.test.assert(false, message + '; ' + mode + ' is an unknown mode');
407 }
420 }
408 };
421 };
409
422
410 casper.select_cell = function(index) {
423 casper.select_cell = function(index) {
411 // Select a cell in the notebook.
424 // Select a cell in the notebook.
412 this.evaluate(function (i) {
425 this.evaluate(function (i) {
413 IPython.notebook.select(i);
426 IPython.notebook.select(i);
414 }, {i: index});
427 }, {i: index});
415 };
428 };
416
429
417 casper.click_cell_editor = function(index) {
430 casper.click_cell_editor = function(index) {
418 // Emulate a click on a cell's editor.
431 // Emulate a click on a cell's editor.
419
432
420 // Code Mirror does not play nicely with emulated brower events.
433 // Code Mirror does not play nicely with emulated brower events.
421 // Instead of trying to emulate a click, here we run code similar to
434 // Instead of trying to emulate a click, here we run code similar to
422 // the code used in Code Mirror that handles the mousedown event on a
435 // the code used in Code Mirror that handles the mousedown event on a
423 // region of codemirror that the user can focus.
436 // region of codemirror that the user can focus.
424 this.evaluate(function (i) {
437 this.evaluate(function (i) {
425 var cm = IPython.notebook.get_cell(i).code_mirror;
438 var cm = IPython.notebook.get_cell(i).code_mirror;
426 if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input)){
439 if (cm.options.readOnly != "nocursor" && (document.activeElement != cm.display.input)){
427 cm.display.input.focus();
440 cm.display.input.focus();
428 }
441 }
429 }, {i: index});
442 }, {i: index});
430 };
443 };
431
444
432 casper.set_cell_editor_cursor = function(index, line_index, char_index) {
445 casper.set_cell_editor_cursor = function(index, line_index, char_index) {
433 // Set the Code Mirror instance cursor's location.
446 // Set the Code Mirror instance cursor's location.
434 this.evaluate(function (i, l, c) {
447 this.evaluate(function (i, l, c) {
435 IPython.notebook.get_cell(i).code_mirror.setCursor(l, c);
448 IPython.notebook.get_cell(i).code_mirror.setCursor(l, c);
436 }, {i: index, l: line_index, c: char_index});
449 }, {i: index, l: line_index, c: char_index});
437 };
450 };
438
451
439 casper.focus_notebook = function() {
452 casper.focus_notebook = function() {
440 // Focus the notebook div.
453 // Focus the notebook div.
441 this.evaluate(function (){
454 this.evaluate(function (){
442 $('#notebook').focus();
455 $('#notebook').focus();
443 }, {});
456 }, {});
444 };
457 };
445
458
446 casper.trigger_keydown = function() {
459 casper.trigger_keydown = function() {
447 // Emulate a keydown in the notebook.
460 // Emulate a keydown in the notebook.
448 for (var i = 0; i < arguments.length; i++) {
461 for (var i = 0; i < arguments.length; i++) {
449 this.evaluate(function (k) {
462 this.evaluate(function (k) {
450 var element = $(document);
463 var element = $(document);
451 var event = IPython.keyboard.shortcut_to_event(k, 'keydown');
464 var event = IPython.keyboard.shortcut_to_event(k, 'keydown');
452 element.trigger(event);
465 element.trigger(event);
453 }, {k: arguments[i]});
466 }, {k: arguments[i]});
454 }
467 }
455 };
468 };
456
469
457 casper.get_keyboard_mode = function() {
470 casper.get_keyboard_mode = function() {
458 // Get the mode of the keyboard manager.
471 // Get the mode of the keyboard manager.
459 return this.evaluate(function() {
472 return this.evaluate(function() {
460 return IPython.keyboard_manager.mode;
473 return IPython.keyboard_manager.mode;
461 }, {});
474 }, {});
462 };
475 };
463
476
464 casper.get_notebook_mode = function() {
477 casper.get_notebook_mode = function() {
465 // Get the mode of the notebook.
478 // Get the mode of the notebook.
466 return this.evaluate(function() {
479 return this.evaluate(function() {
467 return IPython.notebook.mode;
480 return IPython.notebook.mode;
468 }, {});
481 }, {});
469 };
482 };
470
483
471 casper.get_cell = function(index) {
484 casper.get_cell = function(index) {
472 // Get a single cell.
485 // Get a single cell.
473 //
486 //
474 // Note: Handles to DOM elements stored in the cell will be useless once in
487 // Note: Handles to DOM elements stored in the cell will be useless once in
475 // CasperJS context.
488 // CasperJS context.
476 return this.evaluate(function(i) {
489 return this.evaluate(function(i) {
477 var cell = IPython.notebook.get_cell(i);
490 var cell = IPython.notebook.get_cell(i);
478 if (cell) {
491 if (cell) {
479 return cell;
492 return cell;
480 }
493 }
481 return null;
494 return null;
482 }, {i : index});
495 }, {i : index});
483 };
496 };
484
497
485 casper.is_cell_editor_focused = function(index) {
498 casper.is_cell_editor_focused = function(index) {
486 // Make sure a cell's editor is the only editor focused on the page.
499 // Make sure a cell's editor is the only editor focused on the page.
487 return this.evaluate(function(i) {
500 return this.evaluate(function(i) {
488 var focused_textarea = $('#notebook .CodeMirror-focused textarea');
501 var focused_textarea = $('#notebook .CodeMirror-focused textarea');
489 if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; }
502 if (focused_textarea.length > 1) { throw 'More than one Code Mirror editor is focused at once!'; }
490 if (i === null) {
503 if (i === null) {
491 return focused_textarea.length === 0;
504 return focused_textarea.length === 0;
492 } else {
505 } else {
493 var cell = IPython.notebook.get_cell(i);
506 var cell = IPython.notebook.get_cell(i);
494 if (cell) {
507 if (cell) {
495 return cell.code_mirror.getInputField() == focused_textarea[0];
508 return cell.code_mirror.getInputField() == focused_textarea[0];
496 }
509 }
497 }
510 }
498 return false;
511 return false;
499 }, {i : index});
512 }, {i : index});
500 };
513 };
501
514
502 casper.is_only_cell_selected = function(index) {
515 casper.is_only_cell_selected = function(index) {
503 // Check if a cell is the only cell selected.
516 // Check if a cell is the only cell selected.
504 // Pass null as the index to check if no cells are selected.
517 // Pass null as the index to check if no cells are selected.
505 return this.is_only_cell_on(index, 'selected', 'unselected');
518 return this.is_only_cell_on(index, 'selected', 'unselected');
506 };
519 };
507
520
508 casper.is_only_cell_edit = function(index) {
521 casper.is_only_cell_edit = function(index) {
509 // Check if a cell is the only cell in edit mode.
522 // Check if a cell is the only cell in edit mode.
510 // Pass null as the index to check if all of the cells are in command mode.
523 // Pass null as the index to check if all of the cells are in command mode.
511 var cells_length = this.get_cells_length();
524 var cells_length = this.get_cells_length();
512 for (var j = 0; j < cells_length; j++) {
525 for (var j = 0; j < cells_length; j++) {
513 if (j === index) {
526 if (j === index) {
514 if (!this.cell_mode_is(j, 'edit')) {
527 if (!this.cell_mode_is(j, 'edit')) {
515 return false;
528 return false;
516 }
529 }
517 } else {
530 } else {
518 if (this.cell_mode_is(j, 'edit')) {
531 if (this.cell_mode_is(j, 'edit')) {
519 return false;
532 return false;
520 }
533 }
521 }
534 }
522 }
535 }
523 return true;
536 return true;
524 };
537 };
525
538
526 casper.is_only_cell_on = function(i, on_class, off_class) {
539 casper.is_only_cell_on = function(i, on_class, off_class) {
527 // Check if a cell is the only cell with the `on_class` DOM class applied to it.
540 // Check if a cell is the only cell with the `on_class` DOM class applied to it.
528 // All of the other cells are checked for the `off_class` DOM class.
541 // All of the other cells are checked for the `off_class` DOM class.
529 // Pass null as the index to check if all of the cells have the `off_class`.
542 // Pass null as the index to check if all of the cells have the `off_class`.
530 var cells_length = this.get_cells_length();
543 var cells_length = this.get_cells_length();
531 for (var j = 0; j < cells_length; j++) {
544 for (var j = 0; j < cells_length; j++) {
532 if (j === i) {
545 if (j === i) {
533 if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) {
546 if (this.cell_has_class(j, off_class) || !this.cell_has_class(j, on_class)) {
534 return false;
547 return false;
535 }
548 }
536 } else {
549 } else {
537 if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) {
550 if (!this.cell_has_class(j, off_class) || this.cell_has_class(j, on_class)) {
538 return false;
551 return false;
539 }
552 }
540 }
553 }
541 }
554 }
542 return true;
555 return true;
543 };
556 };
544
557
545 casper.cells_modes = function(){
558 casper.cells_modes = function(){
546 return this.evaluate(function(){
559 return this.evaluate(function(){
547 return IPython.notebook.get_cells().map(function(x,c){return x.mode})
560 return IPython.notebook.get_cells().map(function(x,c){return x.mode})
548 }, {});
561 }, {});
549 };
562 };
550
563
551 casper.cell_mode_is = function(index, mode) {
564 casper.cell_mode_is = function(index, mode) {
552 // Check if a cell is in a specific mode
565 // Check if a cell is in a specific mode
553 return this.evaluate(function(i, m) {
566 return this.evaluate(function(i, m) {
554 var cell = IPython.notebook.get_cell(i);
567 var cell = IPython.notebook.get_cell(i);
555 if (cell) {
568 if (cell) {
556 return cell.mode === m;
569 return cell.mode === m;
557 }
570 }
558 return false;
571 return false;
559 }, {i : index, m: mode});
572 }, {i : index, m: mode});
560 };
573 };
561
574
562
575
563 casper.cell_has_class = function(index, classes) {
576 casper.cell_has_class = function(index, classes) {
564 // Check if a cell has a class.
577 // Check if a cell has a class.
565 return this.evaluate(function(i, c) {
578 return this.evaluate(function(i, c) {
566 var cell = IPython.notebook.get_cell(i);
579 var cell = IPython.notebook.get_cell(i);
567 if (cell) {
580 if (cell) {
568 return cell.element.hasClass(c);
581 return cell.element.hasClass(c);
569 }
582 }
570 return false;
583 return false;
571 }, {i : index, c: classes});
584 }, {i : index, c: classes});
572 };
585 };
573
586
574 casper.is_cell_rendered = function (index) {
587 casper.is_cell_rendered = function (index) {
575 return this.evaluate(function(i) {
588 return this.evaluate(function(i) {
576 return !!IPython.notebook.get_cell(i).rendered;
589 return !!IPython.notebook.get_cell(i).rendered;
577 }, {i:index});
590 }, {i:index});
578 };
591 };
579
592
580 casper.assert_colors_equal = function (hex_color, local_color, msg) {
593 casper.assert_colors_equal = function (hex_color, local_color, msg) {
581 // Tests to see if two colors are equal.
594 // Tests to see if two colors are equal.
582 //
595 //
583 // Parameters
596 // Parameters
584 // hex_color: string
597 // hex_color: string
585 // Hexadecimal color code, with or without preceeding hash character.
598 // Hexadecimal color code, with or without preceeding hash character.
586 // local_color: string
599 // local_color: string
587 // Local color representation. Can either be hexadecimal (default for
600 // Local color representation. Can either be hexadecimal (default for
588 // phantom) or rgb (default for slimer).
601 // phantom) or rgb (default for slimer).
589
602
590 // Remove parentheses, hashes, semi-colons, and space characters.
603 // Remove parentheses, hashes, semi-colons, and space characters.
591 hex_color = hex_color.replace(/[\(\); #]/, '');
604 hex_color = hex_color.replace(/[\(\); #]/, '');
592 local_color = local_color.replace(/[\(\); #]/, '');
605 local_color = local_color.replace(/[\(\); #]/, '');
593
606
594 // If the local color is rgb, clean it up and replace
607 // If the local color is rgb, clean it up and replace
595 if (local_color.substr(0,3).toLowerCase() == 'rgb') {
608 if (local_color.substr(0,3).toLowerCase() == 'rgb') {
596 var components = local_color.substr(3).split(',');
609 var components = local_color.substr(3).split(',');
597 local_color = '';
610 local_color = '';
598 for (var i = 0; i < components.length; i++) {
611 for (var i = 0; i < components.length; i++) {
599 var part = parseInt(components[i]).toString(16);
612 var part = parseInt(components[i]).toString(16);
600 while (part.length < 2) part = '0' + part;
613 while (part.length < 2) part = '0' + part;
601 local_color += part;
614 local_color += part;
602 }
615 }
603 }
616 }
604
617
605 this.test.assertEquals(hex_color.toUpperCase(), local_color.toUpperCase(), msg);
618 this.test.assertEquals(hex_color.toUpperCase(), local_color.toUpperCase(), msg);
606 };
619 };
607
620
608 casper.notebook_test = function(test) {
621 casper.notebook_test = function(test) {
609 // Wrap a notebook test to reduce boilerplate.
622 // Wrap a notebook test to reduce boilerplate.
610 this.open_new_notebook();
623 this.open_new_notebook();
611
624
612 // Echo whether or not we are running this test using SlimerJS
625 // Echo whether or not we are running this test using SlimerJS
613 if (this.evaluate(function(){
626 if (this.evaluate(function(){
614 return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
627 return typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
615 })) {
628 })) {
616 console.log('This test is running in SlimerJS.');
629 console.log('This test is running in SlimerJS.');
617 this.slimerjs = true;
630 this.slimerjs = true;
618 }
631 }
619
632
620 // Make sure to remove the onbeforeunload callback. This callback is
633 // Make sure to remove the onbeforeunload callback. This callback is
621 // responsible for the "Are you sure you want to quit?" type messages.
634 // responsible for the "Are you sure you want to quit?" type messages.
622 // PhantomJS ignores these prompts, SlimerJS does not which causes hangs.
635 // PhantomJS ignores these prompts, SlimerJS does not which causes hangs.
623 this.then(function(){
636 this.then(function(){
624 this.evaluate(function(){
637 this.evaluate(function(){
625 window.onbeforeunload = function(){};
638 window.onbeforeunload = function(){};
626 });
639 });
627 });
640 });
628
641
629 this.then(test);
642 this.then(test);
630
643
631 // Kill the kernel and delete the notebook.
644 // Kill the kernel and delete the notebook.
632 this.shutdown_current_kernel();
645 this.shutdown_current_kernel();
633 // This is still broken but shouldn't be a problem for now.
646 // This is still broken but shouldn't be a problem for now.
634 // this.delete_current_notebook();
647 // this.delete_current_notebook();
635
648
636 // This is required to clean up the page we just finished with. If we don't call this
649 // This is required to clean up the page we just finished with. If we don't call this
637 // casperjs will leak file descriptors of all the open WebSockets in that page. We
650 // casperjs will leak file descriptors of all the open WebSockets in that page. We
638 // have to set this.page=null so that next time casper.start runs, it will create a
651 // have to set this.page=null so that next time casper.start runs, it will create a
639 // new page from scratch.
652 // new page from scratch.
640 this.then(function () {
653 this.then(function () {
641 this.page.close();
654 this.page.close();
642 this.page = null;
655 this.page = null;
643 });
656 });
644
657
645 // Run the browser automation.
658 // Run the browser automation.
646 this.run(function() {
659 this.run(function() {
647 this.test.done();
660 this.test.done();
648 });
661 });
649 };
662 };
650
663
651 casper.wait_for_dashboard = function () {
664 casper.wait_for_dashboard = function () {
652 // Wait for the dashboard list to load.
665 // Wait for the dashboard list to load.
653 casper.waitForSelector('.list_item');
666 casper.waitForSelector('.list_item');
654 };
667 };
655
668
656 casper.open_dashboard = function () {
669 casper.open_dashboard = function () {
657 // Start casper by opening the dashboard page.
670 // Start casper by opening the dashboard page.
658 var baseUrl = this.get_notebook_server();
671 var baseUrl = this.get_notebook_server();
659 this.start(baseUrl);
672 this.start(baseUrl);
660 this.waitFor(this.page_loaded);
673 this.waitFor(this.page_loaded);
661 this.wait_for_dashboard();
674 this.wait_for_dashboard();
662 };
675 };
663
676
664 casper.dashboard_test = function (test) {
677 casper.dashboard_test = function (test) {
665 // Open the dashboard page and run a test.
678 // Open the dashboard page and run a test.
666 this.open_dashboard();
679 this.open_dashboard();
667 this.then(test);
680 this.then(test);
668
681
669 this.then(function () {
682 this.then(function () {
670 this.page.close();
683 this.page.close();
671 this.page = null;
684 this.page = null;
672 });
685 });
673
686
674 // Run the browser automation.
687 // Run the browser automation.
675 this.run(function() {
688 this.run(function() {
676 this.test.done();
689 this.test.done();
677 });
690 });
678 };
691 };
679
692
680 // note that this will only work for UNIQUE events -- if you want to
693 // note that this will only work for UNIQUE events -- if you want to
681 // listen for the same event twice, this will not work!
694 // listen for the same event twice, this will not work!
682 casper.event_test = function (name, events, action, timeout) {
695 casper.event_test = function (name, events, action, timeout) {
683
696
684 // set up handlers to listen for each of the events
697 // set up handlers to listen for each of the events
685 this.thenEvaluate(function (events) {
698 this.thenEvaluate(function (events) {
686 var make_handler = function (event) {
699 var make_handler = function (event) {
687 return function () {
700 return function () {
688 IPython._events_triggered.push(event);
701 IPython._events_triggered.push(event);
689 IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
702 IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
690 delete IPython._event_handlers[event];
703 delete IPython._event_handlers[event];
691 };
704 };
692 };
705 };
693 IPython._event_handlers = {};
706 IPython._event_handlers = {};
694 IPython._events_triggered = [];
707 IPython._events_triggered = [];
695 for (var i=0; i < events.length; i++) {
708 for (var i=0; i < events.length; i++) {
696 IPython._event_handlers[events[i]] = make_handler(events[i]);
709 IPython._event_handlers[events[i]] = make_handler(events[i]);
697 IPython.notebook.events.on(events[i], IPython._event_handlers[events[i]]);
710 IPython.notebook.events.on(events[i], IPython._event_handlers[events[i]]);
698 }
711 }
699 }, [events]);
712 }, [events]);
700
713
701 // execute the requested action
714 // execute the requested action
702 this.then(action);
715 this.then(action);
703
716
704 // wait for all the events to be triggered
717 // wait for all the events to be triggered
705 this.waitFor(function () {
718 this.waitFor(function () {
706 return this.evaluate(function (events) {
719 return this.evaluate(function (events) {
707 return IPython._events_triggered.length >= events.length;
720 return IPython._events_triggered.length >= events.length;
708 }, [events]);
721 }, [events]);
709 }, undefined, undefined, timeout);
722 }, undefined, undefined, timeout);
710
723
711 // test that the events were triggered in the proper order
724 // test that the events were triggered in the proper order
712 this.then(function () {
725 this.then(function () {
713 var triggered = this.evaluate(function () {
726 var triggered = this.evaluate(function () {
714 return IPython._events_triggered;
727 return IPython._events_triggered;
715 });
728 });
716 var handlers = this.evaluate(function () {
729 var handlers = this.evaluate(function () {
717 return Object.keys(IPython._event_handlers);
730 return Object.keys(IPython._event_handlers);
718 });
731 });
719 this.test.assertEquals(triggered.length, events.length, name + ': ' + events.length + ' events were triggered');
732 this.test.assertEquals(triggered.length, events.length, name + ': ' + events.length + ' events were triggered');
720 this.test.assertEquals(handlers.length, 0, name + ': all handlers triggered');
733 this.test.assertEquals(handlers.length, 0, name + ': all handlers triggered');
721 for (var i=0; i < events.length; i++) {
734 for (var i=0; i < events.length; i++) {
722 this.test.assertEquals(triggered[i], events[i], name + ': ' + events[i] + ' was triggered');
735 this.test.assertEquals(triggered[i], events[i], name + ': ' + events[i] + ' was triggered');
723 }
736 }
724 });
737 });
725
738
726 // turn off any remaining event listeners
739 // turn off any remaining event listeners
727 this.thenEvaluate(function () {
740 this.thenEvaluate(function () {
728 for (var event in IPython._event_handlers) {
741 for (var event in IPython._event_handlers) {
729 IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
742 IPython.notebook.events.off(event, null, IPython._event_handlers[event]);
730 delete IPython._event_handlers[event];
743 delete IPython._event_handlers[event];
731 }
744 }
732 });
745 });
733 };
746 };
734
747
735 casper.options.waitTimeout=10000;
748 casper.options.waitTimeout=10000;
736 casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
749 casper.on('waitFor.timeout', function onWaitForTimeout(timeout) {
737 this.echo("Timeout for " + casper.get_notebook_server());
750 this.echo("Timeout for " + casper.get_notebook_server());
738 this.echo("Is the notebook server running?");
751 this.echo("Is the notebook server running?");
739 });
752 });
740
753
741 casper.print_log = function () {
754 casper.print_log = function () {
742 // Pass `console.log` calls from page JS to casper.
755 // Pass `console.log` calls from page JS to casper.
743 this.on('remote.message', function(msg) {
756 this.on('remote.message', function(msg) {
744 this.echo('Remote message caught: ' + msg);
757 this.echo('Remote message caught: ' + msg);
745 });
758 });
746 };
759 };
747
760
748 casper.on("page.error", function onError(msg, trace) {
761 casper.on("page.error", function onError(msg, trace) {
749 // show errors in the browser
762 // show errors in the browser
750 this.echo("Page Error");
763 this.echo("Page Error");
751 this.echo(" Message: " + msg.split('\n').join('\n '));
764 this.echo(" Message: " + msg.split('\n').join('\n '));
752 this.echo(" Call stack:");
765 this.echo(" Call stack:");
753 var local_path = this.get_notebook_server();
766 var local_path = this.get_notebook_server();
754 for (var i = 0; i < trace.length; i++) {
767 for (var i = 0; i < trace.length; i++) {
755 var frame = trace[i];
768 var frame = trace[i];
756 var file = frame.file;
769 var file = frame.file;
757 // shorten common phantomjs evaluate url
770 // shorten common phantomjs evaluate url
758 // this will have a different value on slimerjs
771 // this will have a different value on slimerjs
759 if (file === "phantomjs://webpage.evaluate()") {
772 if (file === "phantomjs://webpage.evaluate()") {
760 file = "evaluate";
773 file = "evaluate";
761 }
774 }
762 // remove the version tag from the path
775 // remove the version tag from the path
763 file = file.replace(/(\?v=[0-9abcdef]+)/, '');
776 file = file.replace(/(\?v=[0-9abcdef]+)/, '');
764 // remove the local address from the beginning of the path
777 // remove the local address from the beginning of the path
765 if (file.indexOf(local_path) === 0) {
778 if (file.indexOf(local_path) === 0) {
766 file = file.substr(local_path.length);
779 file = file.substr(local_path.length);
767 }
780 }
768 var frame_text = (frame.function.length > 0) ? " in " + frame.function : "";
781 var frame_text = (frame.function.length > 0) ? " in " + frame.function : "";
769 this.echo(" line " + frame.line + " of " + file + frame_text);
782 this.echo(" line " + frame.line + " of " + file + frame_text);
770 }
783 }
771 });
784 });
772
785
773
786
774 casper.capture_log = function () {
787 casper.capture_log = function () {
775 // show captured errors
788 // show captured errors
776 var captured_log = [];
789 var captured_log = [];
777 var seen_errors = 0;
790 var seen_errors = 0;
778 this.on('remote.message', function(msg) {
791 this.on('remote.message', function(msg) {
779 captured_log.push(msg);
792 captured_log.push(msg);
780 });
793 });
781
794
782 var that = this;
795 var that = this;
783 this.test.on("test.done", function (result) {
796 this.test.on("test.done", function (result) {
784 // test.done runs per-file,
797 // test.done runs per-file,
785 // but suiteResults is per-suite (directory)
798 // but suiteResults is per-suite (directory)
786 var current_errors;
799 var current_errors;
787 if (this.suiteResults) {
800 if (this.suiteResults) {
788 // casper 1.1 has suiteResults
801 // casper 1.1 has suiteResults
789 current_errors = this.suiteResults.countErrors() + this.suiteResults.countFailed();
802 current_errors = this.suiteResults.countErrors() + this.suiteResults.countFailed();
790 } else {
803 } else {
791 // casper 1.0 has testResults instead
804 // casper 1.0 has testResults instead
792 current_errors = this.testResults.failed;
805 current_errors = this.testResults.failed;
793 }
806 }
794
807
795 if (current_errors > seen_errors && captured_log.length > 0) {
808 if (current_errors > seen_errors && captured_log.length > 0) {
796 casper.echo("\nCaptured console.log:");
809 casper.echo("\nCaptured console.log:");
797 for (var i = 0; i < captured_log.length; i++) {
810 for (var i = 0; i < captured_log.length; i++) {
798 var output = String(captured_log[i]).split('\n');
811 var output = String(captured_log[i]).split('\n');
799 for (var j = 0; j < output.length; j++) {
812 for (var j = 0; j < output.length; j++) {
800 casper.echo(" " + output[j]);
813 casper.echo(" " + output[j]);
801 }
814 }
802 }
815 }
803 }
816 }
804
817
805 seen_errors = current_errors;
818 seen_errors = current_errors;
806 captured_log = [];
819 captured_log = [];
807 });
820 });
808 };
821 };
809
822
810 casper.interact = function() {
823 casper.interact = function() {
811 // Start an interactive Javascript console.
824 // Start an interactive Javascript console.
812 var system = require('system');
825 var system = require('system');
813 system.stdout.writeLine('JS interactive console.');
826 system.stdout.writeLine('JS interactive console.');
814 system.stdout.writeLine('Type `exit` to quit.');
827 system.stdout.writeLine('Type `exit` to quit.');
815
828
816 function read_line() {
829 function read_line() {
817 system.stdout.writeLine('JS: ');
830 system.stdout.writeLine('JS: ');
818 var line = system.stdin.readLine();
831 var line = system.stdin.readLine();
819 return line;
832 return line;
820 }
833 }
821
834
822 var input = read_line();
835 var input = read_line();
823 while (input.trim() != 'exit') {
836 while (input.trim() != 'exit') {
824 var output = this.evaluate(function(code) {
837 var output = this.evaluate(function(code) {
825 return String(eval(code));
838 return String(eval(code));
826 }, {code: input});
839 }, {code: input});
827 system.stdout.writeLine('\nOut: ' + output);
840 system.stdout.writeLine('\nOut: ' + output);
828 input = read_line();
841 input = read_line();
829 }
842 }
830 };
843 };
831
844
832 casper.capture_log();
845 casper.capture_log();
@@ -1,191 +1,309 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 index = this.append_cell(
42 index = this.append_cell(
43 'from IPython.html import widgets\n' +
43 ['from IPython.html import widgets',
44 'from IPython.display import display, clear_output\n' +
44 'from IPython.display import display, clear_output',
45 'print("Success")');
45 'print("Success")'].join('\n'));
46 this.execute_cell_then(index);
46 this.execute_cell_then(index);
47
47
48 this.then(function () {
48 this.then(function () {
49
50 // Functions that can be used to test the packing and unpacking APIs
51 var that = this;
52 var test_pack = function (input) {
53 var output = that.evaluate(function(input) {
54 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
55 var results = model._pack_models(input);
56 return results;
57 }, {input: input});
58 that.test.assert(recursive_compare(input, output),
59 JSON.stringify(input) + ' passed through Model._pack_model unchanged');
60 };
61 var test_unpack = function (input) {
62 that.thenEvaluate(function(input) {
63 window.results = undefined;
64 var model = new IPython.WidgetModel(IPython.notebook.kernel.widget_manager, undefined);
65 model._unpack_models(input).then(function(results) {
66 window.results = results;
67 });
68 }, {input: input});
69
70 that.waitFor(function check() {
71 return that.evaluate(function() {
72 return window.results;
73 });
74 });
75
76 that.then(function() {
77 var results = that.evaluate(function() {
78 return window.results;
79 });
80 that.test.assert(recursive_compare(input, results),
81 JSON.stringify(input) + ' passed through Model._unpack_model unchanged');
82 });
83 };
84 var test_packing = function(input) {
85 test_pack(input);
86 test_unpack(input);
87 };
88
89 test_packing({0: 'hi', 1: 'bye'});
90 test_packing(['hi', 'bye']);
91 test_packing(['hi', 5]);
92 test_packing(['hi', '5']);
93 test_packing([1.0, 0]);
94 test_packing([1.0, false]);
95 test_packing([1, false]);
96 test_packing([1, false, {a: 'hi'}]);
97 test_packing([1, false, ['hi']]);
98 test_packing([String('hi'), Date("Thu Nov 13 2014 13:46:21 GMT-0500")])
99
100 // Test multi-set, single touch code. First create a custom widget.
49 // Test multi-set, single touch code. First create a custom widget.
101 this.thenEvaluate(function() {
50 this.thenEvaluate(function() {
102 var MultiSetView = IPython.DOMWidgetView.extend({
51 var MultiSetView = IPython.DOMWidgetView.extend({
103 render: function(){
52 render: function(){
104 this.model.set('a', 1);
53 this.model.set('a', 1);
105 this.model.set('b', 2);
54 this.model.set('b', 2);
106 this.model.set('c', 3);
55 this.model.set('c', 3);
107 this.touch();
56 this.touch();
108 },
57 },
109 });
58 });
110 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
59 IPython.WidgetManager.register_widget_view('MultiSetView', MultiSetView);
111 }, {});
60 }, {});
112 });
61 });
113
62
114 // Try creating the multiset widget, verify that sets the values correctly.
63 // Try creating the multiset widget, verify that sets the values correctly.
115 var multiset = {};
64 var multiset = {};
116 multiset.index = this.append_cell(
65 multiset.index = this.append_cell([
117 'from IPython.utils.traitlets import Unicode, CInt\n' +
66 'from IPython.utils.traitlets import Unicode, CInt',
118 'class MultiSetWidget(widgets.Widget):\n' +
67 'class MultiSetWidget(widgets.Widget):',
119 ' _view_name = Unicode("MultiSetView", sync=True)\n' +
68 ' _view_name = Unicode("MultiSetView", sync=True)',
120 ' a = CInt(0, sync=True)\n' +
69 ' a = CInt(0, sync=True)',
121 ' b = CInt(0, sync=True)\n' +
70 ' b = CInt(0, sync=True)',
122 ' c = CInt(0, sync=True)\n' +
71 ' c = CInt(0, sync=True)',
123 ' d = CInt(-1, sync=True)\n' + // See if it sends a full state.
72 ' d = CInt(-1, sync=True)', // See if it sends a full state.
124 ' def set_state(self, sync_data):\n' +
73 ' def set_state(self, sync_data):',
125 ' widgets.Widget.set_state(self, sync_data)\n'+
74 ' widgets.Widget.set_state(self, sync_data)',
126 ' self.d = len(sync_data)\n' +
75 ' self.d = len(sync_data)',
127 'multiset = MultiSetWidget()\n' +
76 'multiset = MultiSetWidget()',
128 'display(multiset)\n' +
77 'display(multiset)',
129 'print(multiset.model_id)');
78 'print(multiset.model_id)'].join('\n'));
130 this.execute_cell_then(multiset.index, function(index) {
79 this.execute_cell_then(multiset.index, function(index) {
131 multiset.model_id = this.get_output_cell(index).text.trim();
80 multiset.model_id = this.get_output_cell(index).text.trim();
132 });
81 });
133
82
134 this.wait_for_widget(multiset);
83 this.wait_for_widget(multiset);
135
84
136 index = this.append_cell(
85 index = this.append_cell(
137 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
86 'print("%d%d%d" % (multiset.a, multiset.b, multiset.c))');
138 this.execute_cell_then(index, function(index) {
87 this.execute_cell_then(index, function(index) {
139 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
88 this.test.assertEquals(this.get_output_cell(index).text.trim(), '123',
140 'Multiple model.set calls and one view.touch update state in back-end.');
89 'Multiple model.set calls and one view.touch update state in back-end.');
141 });
90 });
142
91
143 index = this.append_cell(
92 index = this.append_cell(
144 'print("%d" % (multiset.d))');
93 'print("%d" % (multiset.d))');
145 this.execute_cell_then(index, function(index) {
94 this.execute_cell_then(index, function(index) {
146 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
95 this.test.assertEquals(this.get_output_cell(index).text.trim(), '3',
147 'Multiple model.set calls sent a partial state.');
96 'Multiple model.set calls sent a partial state.');
148 });
97 });
149
98
150 var textbox = {};
99 var textbox = {};
151 throttle_index = this.append_cell(
100 throttle_index = this.append_cell([
152 'import time\n' +
101 'import time',
153 'textbox = widgets.Text()\n' +
102 'textbox = widgets.Text()',
154 'display(textbox)\n' +
103 'display(textbox)',
155 'textbox._dom_classes = ["my-throttle-textbox"]\n' +
104 'textbox._dom_classes = ["my-throttle-textbox"]',
156 'def handle_change(name, old, new):\n' +
105 'def handle_change(name, old, new):',
157 ' display(len(new))\n' +
106 ' display(len(new))',
158 ' time.sleep(0.5)\n' +
107 ' time.sleep(0.5)',
159 'textbox.on_trait_change(handle_change, "value")\n' +
108 'textbox.on_trait_change(handle_change, "value")',
160 'print(textbox.model_id)');
109 'print(textbox.model_id)'].join('\n'));
161 this.execute_cell_then(throttle_index, function(index){
110 this.execute_cell_then(throttle_index, function(index){
162 textbox.model_id = this.get_output_cell(index).text.trim();
111 textbox.model_id = this.get_output_cell(index).text.trim();
163
112
164 this.test.assert(this.cell_element_exists(index,
113 this.test.assert(this.cell_element_exists(index,
165 '.widget-area .widget-subarea'),
114 '.widget-area .widget-subarea'),
166 'Widget subarea exists.');
115 'Widget subarea exists.');
167
116
168 this.test.assert(this.cell_element_exists(index,
117 this.test.assert(this.cell_element_exists(index,
169 '.my-throttle-textbox'), 'Textbox exists.');
118 '.my-throttle-textbox'), 'Textbox exists.');
170
119
171 // Send 20 characters
120 // Send 20 characters
172 this.sendKeys('.my-throttle-textbox input', '....................');
121 this.sendKeys('.my-throttle-textbox input', '12345678901234567890');
173 });
122 });
174
123
175 this.wait_for_widget(textbox);
124 this.wait_for_widget(textbox);
176
125
177 this.then(function () {
126 this.then(function () {
178 var outputs = this.evaluate(function(i) {
127 var outputs = this.evaluate(function(i) {
179 return IPython.notebook.get_cell(i).output_area.outputs;
128 return IPython.notebook.get_cell(i).output_area.outputs;
180 }, {i : throttle_index});
129 }, {i : throttle_index});
181
130
182 // Only 4 outputs should have printed, but because of timing, sometimes
131 // Only 4 outputs should have printed, but because of timing, sometimes
183 // 5 outputs will print. All we need to do is verify num outputs <= 5
132 // 5 outputs will print. All we need to do is verify num outputs <= 5
184 // because that is much less than 20.
133 // because that is much less than 20.
185 this.test.assert(outputs.length <= 5, 'Messages throttled.');
134 this.test.assert(outputs.length <= 5, 'Messages throttled.');
186
135
187 // We also need to verify that the last state sent was correct.
136 // We also need to verify that the last state sent was correct.
188 var last_state = outputs[outputs.length-1].data['text/plain'];
137 var last_state = outputs[outputs.length-1].data['text/plain'];
189 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
138 this.test.assertEquals(last_state, "20", "Last state sent when throttling.");
190 });
139 });
140
141
142 this.thenEvaluate(function() {
143 define('TestWidget', ['widgets/js/widget', 'base/js/utils', 'underscore'], function(widget, utils, _) {
144 var floatArray = {
145 deserialize: function (value, model) {
146 if (value===null) {return null;}
147 // DataView -> float64 typed array
148 return new Float64Array(value.buffer);
149 },
150 // serialization automatically handled since the
151 // attribute is an ArrayBuffer view
152 };
153
154 var floatList = {
155 deserialize: function (value, model) {
156 // list of floats -> list of strings
157 return value.map(function(x) {return x.toString()});
158 },
159 serialize: function(value, model) {
160 // list of strings -> list of floats
161 return value.map(function(x) {return parseFloat(x);})
162 }
163 };
164
165 var TestWidgetModel = widget.WidgetModel.extend({}, {
166 serializers: _.extend({
167 array_list: floatList,
168 array_binary: floatArray
169 }, widget.WidgetModel.serializers)
170 });
171
172 var TestWidgetView = widget.DOMWidgetView.extend({
173 render: function () {
174 this.listenTo(this.model, 'msg:custom', this.handle_msg);
175 },
176 handle_msg: function(content, buffers) {
177 this.msg = [content, buffers];
178 }
179 });
180
181 return {TestWidgetModel: TestWidgetModel, TestWidgetView: TestWidgetView};
182 });
183 });
184
185 var testwidget = {};
186 this.append_cell_execute_then([
187 'from IPython.html import widgets',
188 'from IPython.utils.traitlets import Unicode, Instance, List',
189 'from IPython.display import display',
190 'from array import array',
191 'def _array_to_memoryview(x):',
192 ' if x is None: return None',
193 ' try:',
194 ' y = memoryview(x)',
195 ' except TypeError:',
196 ' # in python 2, arrays do not support the new buffer protocol',
197 ' y = memoryview(buffer(x))',
198 ' return y',
199 'def _memoryview_to_array(x):',
200 ' if x is None: return None',
201 ' return array("d", x.tobytes())',
202 'arrays_binary = {',
203 ' "from_json": _memoryview_to_array,',
204 ' "to_json": _array_to_memoryview',
205 '}',
206 '',
207 'def _array_to_list(x):',
208 ' return list(x)',
209 'def _list_to_array(x):',
210 ' return array("d",x)',
211 'arrays_list = {',
212 ' "from_json": _list_to_array,',
213 ' "to_json": _array_to_list',
214 '}',
215 '',
216 'class TestWidget(widgets.DOMWidget):',
217 ' _model_module = Unicode("TestWidget", sync=True)',
218 ' _model_name = Unicode("TestWidgetModel", sync=True)',
219 ' _view_module = Unicode("TestWidget", sync=True)',
220 ' _view_name = Unicode("TestWidgetView", sync=True)',
221 ' array_binary = Instance(array, allow_none=True, sync=True, **arrays_binary)',
222 ' array_list = Instance(array, args=("d", [3.0]), allow_none=False, sync=True, **arrays_list)',
223 ' msg = {}',
224 ' def __init__(self, **kwargs):',
225 ' super(widgets.DOMWidget, self).__init__(**kwargs)',
226 ' self.on_msg(self._msg)',
227 ' def _msg(self, _, content, buffers):',
228 ' self.msg = [content, buffers]',
229 'x=TestWidget()',
230 'display(x)',
231 'print(x.model_id)'].join('\n'), function(index){
232 testwidget.index = index;
233 testwidget.model_id = this.get_output_cell(index).text.trim();
234 });
235 this.wait_for_widget(testwidget);
236
237
238 this.append_cell_execute_then('x.array_list = array("d", [1.5, 2.0, 3.1])');
239 this.wait_for_widget(testwidget);
240 this.then(function() {
241 var result = this.evaluate(function(index) {
242 var v = IPython.notebook.get_cell(index).widget_views[0];
243 var result = v.model.get('array_list');
244 var z = result.slice();
245 z[0]+="1234";
246 z[1]+="5678";
247 v.model.set('array_list', z);
248 v.touch();
249 return result;
250 }, testwidget.index);
251 this.test.assertEquals(result, ["1.5", "2", "3.1"], "JSON custom serializer kernel -> js");
252 });
253
254 this.assert_output_equals('print(x.array_list.tolist() == [1.51234, 25678.0, 3.1])',
255 'True', 'JSON custom serializer js -> kernel');
256
257 if (this.slimerjs) {
258 this.append_cell_execute_then("x.array_binary=array('d', [1.5,2.5,5])", function() {
259 this.evaluate(function(index) {
260 var v = IPython.notebook.get_cell(index).widget_views[0];
261 var z = v.model.get('array_binary');
262 z[0]*=3;
263 z[1]*=3;
264 z[2]*=3;
265 // we set to null so that we recognize the change
266 // when we set data back to z
267 v.model.set('array_binary', null);
268 v.model.set('array_binary', z);
269 v.touch();
270 }, textwidget.index);
271 });
272 this.wait_for_widget(testwidget);
273 this.assert_output_equals('x.array_binary.tolist() == [4.5, 7.5, 15.0]',
274 'True\n', 'Binary custom serializer js -> kernel')
275
276 this.append_cell_execute_then('x.send("some content", [memoryview(b"binarycontent"), memoryview("morecontent")])');
277 this.wait_for_widget(testwidget);
278
279 this.then(function() {
280 var result = this.evaluate(function(index) {
281 var v = IPython.notebook.get_cell(index).widget_views[0];
282 var d = new TextDecoder('utf-8');
283 return {text: v.msg[0],
284 binary0: d.decode(v.msg[1][0]),
285 binary1: d.decode(v.msg[1][1])};
286 }, testwidget.index);
287 this.test.assertEquals(result, {text: 'some content',
288 binary0: 'binarycontent',
289 binary1: 'morecontent'},
290 "Binary widget messages kernel -> js");
291 });
292
293 this.then(function() {
294 this.evaluate(function(index) {
295 var v = IPython.notebook.get_cell(index).widget_views[0];
296 v.send('content back', [new Uint8Array([1,2,3,4]), new Float64Array([2.1828, 3.14159])])
297 }, testwidget.index);
298 });
299 this.wait_for_widget(testwidget);
300 this.assert_output_equals([
301 'all([x.msg[0] == "content back",',
302 ' x.msg[1][0].tolist() == [1,2,3,4],',
303 ' array("d", x.msg[1][1].tobytes()).tolist() == [2.1828, 3.14159]])'].join('\n'),
304 'True', 'Binary buffers message js -> kernel');
305 } else {
306 console.log("skipping binary websocket tests on phantomjs");
307 }
308
191 });
309 });
@@ -1,497 +1,497 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16 import collections
16 import collections
17
17
18 from IPython.core.getipython import get_ipython
18 from IPython.core.getipython import get_ipython
19 from IPython.kernel.comm import Comm
19 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
20 from IPython.config import LoggingConfigurable
21 from IPython.utils.importstring import import_item
21 from IPython.utils.importstring import import_item
22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 from IPython.utils.py3compat import string_types
24 from IPython.utils.py3compat import string_types
25 from .trait_types import Color
25 from .trait_types import Color
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Classes
28 # Classes
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30 class CallbackDispatcher(LoggingConfigurable):
30 class CallbackDispatcher(LoggingConfigurable):
31 """A structure for registering and running callbacks"""
31 """A structure for registering and running callbacks"""
32 callbacks = List()
32 callbacks = List()
33
33
34 def __call__(self, *args, **kwargs):
34 def __call__(self, *args, **kwargs):
35 """Call all of the registered callbacks."""
35 """Call all of the registered callbacks."""
36 value = None
36 value = None
37 for callback in self.callbacks:
37 for callback in self.callbacks:
38 try:
38 try:
39 local_value = callback(*args, **kwargs)
39 local_value = callback(*args, **kwargs)
40 except Exception as e:
40 except Exception as e:
41 ip = get_ipython()
41 ip = get_ipython()
42 if ip is None:
42 if ip is None:
43 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
43 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
44 else:
44 else:
45 ip.showtraceback()
45 ip.showtraceback()
46 else:
46 else:
47 value = local_value if local_value is not None else value
47 value = local_value if local_value is not None else value
48 return value
48 return value
49
49
50 def register_callback(self, callback, remove=False):
50 def register_callback(self, callback, remove=False):
51 """(Un)Register a callback
51 """(Un)Register a callback
52
52
53 Parameters
53 Parameters
54 ----------
54 ----------
55 callback: method handle
55 callback: method handle
56 Method to be registered or unregistered.
56 Method to be registered or unregistered.
57 remove=False: bool
57 remove=False: bool
58 Whether to unregister the callback."""
58 Whether to unregister the callback."""
59
59
60 # (Un)Register the callback.
60 # (Un)Register the callback.
61 if remove and callback in self.callbacks:
61 if remove and callback in self.callbacks:
62 self.callbacks.remove(callback)
62 self.callbacks.remove(callback)
63 elif not remove and callback not in self.callbacks:
63 elif not remove and callback not in self.callbacks:
64 self.callbacks.append(callback)
64 self.callbacks.append(callback)
65
65
66 def _show_traceback(method):
66 def _show_traceback(method):
67 """decorator for showing tracebacks in IPython"""
67 """decorator for showing tracebacks in IPython"""
68 def m(self, *args, **kwargs):
68 def m(self, *args, **kwargs):
69 try:
69 try:
70 return(method(self, *args, **kwargs))
70 return(method(self, *args, **kwargs))
71 except Exception as e:
71 except Exception as e:
72 ip = get_ipython()
72 ip = get_ipython()
73 if ip is None:
73 if ip is None:
74 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
74 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
75 else:
75 else:
76 ip.showtraceback()
76 ip.showtraceback()
77 return m
77 return m
78
78
79
79
80 def register(key=None):
80 def register(key=None):
81 """Returns a decorator registering a widget class in the widget registry.
81 """Returns a decorator registering a widget class in the widget registry.
82 If no key is provided, the class name is used as a key. A key is
82 If no key is provided, the class name is used as a key. A key is
83 provided for each core IPython widget so that the frontend can use
83 provided for each core IPython widget so that the frontend can use
84 this key regardless of the language of the kernel"""
84 this key regardless of the language of the kernel"""
85 def wrap(widget):
85 def wrap(widget):
86 l = key if key is not None else widget.__module__ + widget.__name__
86 l = key if key is not None else widget.__module__ + widget.__name__
87 Widget.widget_types[l] = widget
87 Widget.widget_types[l] = widget
88 return widget
88 return widget
89 return wrap
89 return wrap
90
90
91
91
92 class Widget(LoggingConfigurable):
92 class Widget(LoggingConfigurable):
93 #-------------------------------------------------------------------------
93 #-------------------------------------------------------------------------
94 # Class attributes
94 # Class attributes
95 #-------------------------------------------------------------------------
95 #-------------------------------------------------------------------------
96 _widget_construction_callback = None
96 _widget_construction_callback = None
97 widgets = {}
97 widgets = {}
98 widget_types = {}
98 widget_types = {}
99
99
100 @staticmethod
100 @staticmethod
101 def on_widget_constructed(callback):
101 def on_widget_constructed(callback):
102 """Registers a callback to be called when a widget is constructed.
102 """Registers a callback to be called when a widget is constructed.
103
103
104 The callback must have the following signature:
104 The callback must have the following signature:
105 callback(widget)"""
105 callback(widget)"""
106 Widget._widget_construction_callback = callback
106 Widget._widget_construction_callback = callback
107
107
108 @staticmethod
108 @staticmethod
109 def _call_widget_constructed(widget):
109 def _call_widget_constructed(widget):
110 """Static method, called when a widget is constructed."""
110 """Static method, called when a widget is constructed."""
111 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
111 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
112 Widget._widget_construction_callback(widget)
112 Widget._widget_construction_callback(widget)
113
113
114 @staticmethod
114 @staticmethod
115 def handle_comm_opened(comm, msg):
115 def handle_comm_opened(comm, msg):
116 """Static method, called when a widget is constructed."""
116 """Static method, called when a widget is constructed."""
117 widget_class = import_item(msg['content']['data']['widget_class'])
117 widget_class = import_item(msg['content']['data']['widget_class'])
118 widget = widget_class(comm=comm)
118 widget = widget_class(comm=comm)
119
119
120
120
121 #-------------------------------------------------------------------------
121 #-------------------------------------------------------------------------
122 # Traits
122 # Traits
123 #-------------------------------------------------------------------------
123 #-------------------------------------------------------------------------
124 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
124 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
125 in which to find _model_name. If empty, look in the global registry.""")
125 in which to find _model_name. If empty, look in the global registry.""")
126 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
126 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
127 registered in the front-end to create and sync this widget with.""")
127 registered in the front-end to create and sync this widget with.""")
128 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
128 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
129 If empty, look in the global registry.""", sync=True)
129 If empty, look in the global registry.""", sync=True)
130 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
130 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
131 to use to represent the widget.""", sync=True)
131 to use to represent the widget.""", sync=True)
132 comm = Instance('IPython.kernel.comm.Comm', allow_none=True)
132 comm = Instance('IPython.kernel.comm.Comm', allow_none=True)
133
133
134 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
134 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
135 front-end can send before receiving an idle msg from the back-end.""")
135 front-end can send before receiving an idle msg from the back-end.""")
136
136
137 version = Int(0, sync=True, help="""Widget's version""")
137 version = Int(0, sync=True, help="""Widget's version""")
138 keys = List()
138 keys = List()
139 def _keys_default(self):
139 def _keys_default(self):
140 return [name for name in self.traits(sync=True)]
140 return [name for name in self.traits(sync=True)]
141
141
142 _property_lock = Tuple((None, None))
142 _property_lock = Tuple((None, None))
143 _send_state_lock = Int(0)
143 _send_state_lock = Int(0)
144 _states_to_send = Set()
144 _states_to_send = Set()
145 _display_callbacks = Instance(CallbackDispatcher, ())
145 _display_callbacks = Instance(CallbackDispatcher, ())
146 _msg_callbacks = Instance(CallbackDispatcher, ())
146 _msg_callbacks = Instance(CallbackDispatcher, ())
147
147
148 #-------------------------------------------------------------------------
148 #-------------------------------------------------------------------------
149 # (Con/de)structor
149 # (Con/de)structor
150 #-------------------------------------------------------------------------
150 #-------------------------------------------------------------------------
151 def __init__(self, **kwargs):
151 def __init__(self, **kwargs):
152 """Public constructor"""
152 """Public constructor"""
153 self._model_id = kwargs.pop('model_id', None)
153 self._model_id = kwargs.pop('model_id', None)
154 super(Widget, self).__init__(**kwargs)
154 super(Widget, self).__init__(**kwargs)
155
155
156 Widget._call_widget_constructed(self)
156 Widget._call_widget_constructed(self)
157 self.open()
157 self.open()
158
158
159 def __del__(self):
159 def __del__(self):
160 """Object disposal"""
160 """Object disposal"""
161 self.close()
161 self.close()
162
162
163 #-------------------------------------------------------------------------
163 #-------------------------------------------------------------------------
164 # Properties
164 # Properties
165 #-------------------------------------------------------------------------
165 #-------------------------------------------------------------------------
166
166
167 def open(self):
167 def open(self):
168 """Open a comm to the frontend if one isn't already open."""
168 """Open a comm to the frontend if one isn't already open."""
169 if self.comm is None:
169 if self.comm is None:
170 args = dict(target_name='ipython.widget',
170 args = dict(target_name='ipython.widget',
171 data={'model_name': self._model_name,
171 data={'model_name': self._model_name,
172 'model_module': self._model_module})
172 'model_module': self._model_module})
173 if self._model_id is not None:
173 if self._model_id is not None:
174 args['comm_id'] = self._model_id
174 args['comm_id'] = self._model_id
175 self.comm = Comm(**args)
175 self.comm = Comm(**args)
176
176
177 def _comm_changed(self, name, new):
177 def _comm_changed(self, name, new):
178 """Called when the comm is changed."""
178 """Called when the comm is changed."""
179 if new is None:
179 if new is None:
180 return
180 return
181 self._model_id = self.model_id
181 self._model_id = self.model_id
182
182
183 self.comm.on_msg(self._handle_msg)
183 self.comm.on_msg(self._handle_msg)
184 Widget.widgets[self.model_id] = self
184 Widget.widgets[self.model_id] = self
185
185
186 # first update
186 # first update
187 self.send_state()
187 self.send_state()
188
188
189 @property
189 @property
190 def model_id(self):
190 def model_id(self):
191 """Gets the model id of this widget.
191 """Gets the model id of this widget.
192
192
193 If a Comm doesn't exist yet, a Comm will be created automagically."""
193 If a Comm doesn't exist yet, a Comm will be created automagically."""
194 return self.comm.comm_id
194 return self.comm.comm_id
195
195
196 #-------------------------------------------------------------------------
196 #-------------------------------------------------------------------------
197 # Methods
197 # Methods
198 #-------------------------------------------------------------------------
198 #-------------------------------------------------------------------------
199
199
200 def close(self):
200 def close(self):
201 """Close method.
201 """Close method.
202
202
203 Closes the underlying comm.
203 Closes the underlying comm.
204 When the comm is closed, all of the widget views are automatically
204 When the comm is closed, all of the widget views are automatically
205 removed from the front-end."""
205 removed from the front-end."""
206 if self.comm is not None:
206 if self.comm is not None:
207 Widget.widgets.pop(self.model_id, None)
207 Widget.widgets.pop(self.model_id, None)
208 self.comm.close()
208 self.comm.close()
209 self.comm = None
209 self.comm = None
210
210
211 def send_state(self, key=None):
211 def send_state(self, key=None):
212 """Sends the widget state, or a piece of it, to the front-end.
212 """Sends the widget state, or a piece of it, to the front-end.
213
213
214 Parameters
214 Parameters
215 ----------
215 ----------
216 key : unicode, or iterable (optional)
216 key : unicode, or iterable (optional)
217 A single property's name or iterable of property names to sync with the front-end.
217 A single property's name or iterable of property names to sync with the front-end.
218 """
218 """
219 self._send({
219 state, buffer_keys, buffers = self.get_state(key=key)
220 "method" : "update",
220 msg = {"method": "update", "state": state}
221 "state" : self.get_state(key=key)
221 if buffer_keys:
222 })
222 msg['buffers'] = buffer_keys
223 self._send(msg, buffers=buffers)
223
224
224 def get_state(self, key=None):
225 def get_state(self, key=None):
225 """Gets the widget state, or a piece of it.
226 """Gets the widget state, or a piece of it.
226
227
227 Parameters
228 Parameters
228 ----------
229 ----------
229 key : unicode or iterable (optional)
230 key : unicode or iterable (optional)
230 A single property's name or iterable of property names to get.
231 A single property's name or iterable of property names to get.
232
233 Returns
234 -------
235 state : dict of states
236 buffer_keys : list of strings
237 the values that are stored in buffers
238 buffers : list of binary memoryviews
239 values to transmit in binary
240 metadata : dict
241 metadata for each field: {key: metadata}
231 """
242 """
232 if key is None:
243 if key is None:
233 keys = self.keys
244 keys = self.keys
234 elif isinstance(key, string_types):
245 elif isinstance(key, string_types):
235 keys = [key]
246 keys = [key]
236 elif isinstance(key, collections.Iterable):
247 elif isinstance(key, collections.Iterable):
237 keys = key
248 keys = key
238 else:
249 else:
239 raise ValueError("key must be a string, an iterable of keys, or None")
250 raise ValueError("key must be a string, an iterable of keys, or None")
240 state = {}
251 state = {}
252 buffers = []
253 buffer_keys = []
241 for k in keys:
254 for k in keys:
242 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
255 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
243 value = getattr(self, k)
256 value = getattr(self, k)
244 state[k] = f(value)
257 serialized = f(value)
245 return state
258 if isinstance(serialized, memoryview):
259 buffers.append(serialized)
260 buffer_keys.append(k)
261 else:
262 state[k] = serialized
263 return state, buffer_keys, buffers
246
264
247 def set_state(self, sync_data):
265 def set_state(self, sync_data):
248 """Called when a state is received from the front-end."""
266 """Called when a state is received from the front-end."""
249 for name in self.keys:
267 for name in self.keys:
250 if name in sync_data:
268 if name in sync_data:
251 json_value = sync_data[name]
269 json_value = sync_data[name]
252 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
270 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
253 with self._lock_property(name, json_value):
271 with self._lock_property(name, json_value):
254 setattr(self, name, from_json(json_value))
272 setattr(self, name, from_json(json_value))
255
273
256 def send(self, content):
274 def send(self, content, buffers=None):
257 """Sends a custom msg to the widget model in the front-end.
275 """Sends a custom msg to the widget model in the front-end.
258
276
259 Parameters
277 Parameters
260 ----------
278 ----------
261 content : dict
279 content : dict
262 Content of the message to send.
280 Content of the message to send.
281 buffers : list of binary buffers
282 Binary buffers to send with message
263 """
283 """
264 self._send({"method": "custom", "content": content})
284 self._send({"method": "custom", "content": content}, buffers=buffers)
265
285
266 def on_msg(self, callback, remove=False):
286 def on_msg(self, callback, remove=False):
267 """(Un)Register a custom msg receive callback.
287 """(Un)Register a custom msg receive callback.
268
288
269 Parameters
289 Parameters
270 ----------
290 ----------
271 callback: callable
291 callback: callable
272 callback will be passed two arguments when a message arrives::
292 callback will be passed three arguments when a message arrives::
273
293
274 callback(widget, content)
294 callback(widget, content, buffers)
275
295
276 remove: bool
296 remove: bool
277 True if the callback should be unregistered."""
297 True if the callback should be unregistered."""
278 self._msg_callbacks.register_callback(callback, remove=remove)
298 self._msg_callbacks.register_callback(callback, remove=remove)
279
299
280 def on_displayed(self, callback, remove=False):
300 def on_displayed(self, callback, remove=False):
281 """(Un)Register a widget displayed callback.
301 """(Un)Register a widget displayed callback.
282
302
283 Parameters
303 Parameters
284 ----------
304 ----------
285 callback: method handler
305 callback: method handler
286 Must have a signature of::
306 Must have a signature of::
287
307
288 callback(widget, **kwargs)
308 callback(widget, **kwargs)
289
309
290 kwargs from display are passed through without modification.
310 kwargs from display are passed through without modification.
291 remove: bool
311 remove: bool
292 True if the callback should be unregistered."""
312 True if the callback should be unregistered."""
293 self._display_callbacks.register_callback(callback, remove=remove)
313 self._display_callbacks.register_callback(callback, remove=remove)
294
314
295 def add_trait(self, traitname, trait):
315 def add_trait(self, traitname, trait):
296 """Dynamically add a trait attribute to the Widget."""
316 """Dynamically add a trait attribute to the Widget."""
297 super(Widget, self).add_trait(traitname, trait)
317 super(Widget, self).add_trait(traitname, trait)
298 if trait.get_metadata('sync'):
318 if trait.get_metadata('sync'):
299 self.keys.append(traitname)
319 self.keys.append(traitname)
300 self.send_state(traitname)
320 self.send_state(traitname)
301
321
302 #-------------------------------------------------------------------------
322 #-------------------------------------------------------------------------
303 # Support methods
323 # Support methods
304 #-------------------------------------------------------------------------
324 #-------------------------------------------------------------------------
305 @contextmanager
325 @contextmanager
306 def _lock_property(self, key, value):
326 def _lock_property(self, key, value):
307 """Lock a property-value pair.
327 """Lock a property-value pair.
308
328
309 The value should be the JSON state of the property.
329 The value should be the JSON state of the property.
310
330
311 NOTE: This, in addition to the single lock for all state changes, is
331 NOTE: This, in addition to the single lock for all state changes, is
312 flawed. In the future we may want to look into buffering state changes
332 flawed. In the future we may want to look into buffering state changes
313 back to the front-end."""
333 back to the front-end."""
314 self._property_lock = (key, value)
334 self._property_lock = (key, value)
315 try:
335 try:
316 yield
336 yield
317 finally:
337 finally:
318 self._property_lock = (None, None)
338 self._property_lock = (None, None)
319
339
320 @contextmanager
340 @contextmanager
321 def hold_sync(self):
341 def hold_sync(self):
322 """Hold syncing any state until the context manager is released"""
342 """Hold syncing any state until the context manager is released"""
323 # We increment a value so that this can be nested. Syncing will happen when
343 # We increment a value so that this can be nested. Syncing will happen when
324 # all levels have been released.
344 # all levels have been released.
325 self._send_state_lock += 1
345 self._send_state_lock += 1
326 try:
346 try:
327 yield
347 yield
328 finally:
348 finally:
329 self._send_state_lock -=1
349 self._send_state_lock -=1
330 if self._send_state_lock == 0:
350 if self._send_state_lock == 0:
331 self.send_state(self._states_to_send)
351 self.send_state(self._states_to_send)
332 self._states_to_send.clear()
352 self._states_to_send.clear()
333
353
334 def _should_send_property(self, key, value):
354 def _should_send_property(self, key, value):
335 """Check the property lock (property_lock)"""
355 """Check the property lock (property_lock)"""
336 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
356 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
337 if (key == self._property_lock[0]
357 if (key == self._property_lock[0]
338 and to_json(value) == self._property_lock[1]):
358 and to_json(value) == self._property_lock[1]):
339 return False
359 return False
340 elif self._send_state_lock > 0:
360 elif self._send_state_lock > 0:
341 self._states_to_send.add(key)
361 self._states_to_send.add(key)
342 return False
362 return False
343 else:
363 else:
344 return True
364 return True
345
365
346 # Event handlers
366 # Event handlers
347 @_show_traceback
367 @_show_traceback
348 def _handle_msg(self, msg):
368 def _handle_msg(self, msg):
349 """Called when a msg is received from the front-end"""
369 """Called when a msg is received from the front-end"""
350 data = msg['content']['data']
370 data = msg['content']['data']
351 method = data['method']
371 method = data['method']
352
372
353 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
373 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
354 if method == 'backbone':
374 if method == 'backbone':
355 if 'sync_data' in data:
375 if 'sync_data' in data:
376 # get binary buffers too
356 sync_data = data['sync_data']
377 sync_data = data['sync_data']
378 for i,k in enumerate(data.get('buffer_keys', [])):
379 sync_data[k] = msg['buffers'][i]
357 self.set_state(sync_data) # handles all methods
380 self.set_state(sync_data) # handles all methods
358
381
359 # Handle a state request.
382 # Handle a state request.
360 elif method == 'request_state':
383 elif method == 'request_state':
361 self.send_state()
384 self.send_state()
362
385
363 # Handle a custom msg from the front-end.
386 # Handle a custom msg from the front-end.
364 elif method == 'custom':
387 elif method == 'custom':
365 if 'content' in data:
388 if 'content' in data:
366 self._handle_custom_msg(data['content'])
389 self._handle_custom_msg(data['content'], msg['buffers'])
367
390
368 # Catch remainder.
391 # Catch remainder.
369 else:
392 else:
370 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
393 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
371
394
372 def _handle_custom_msg(self, content):
395 def _handle_custom_msg(self, content, buffers):
373 """Called when a custom msg is received."""
396 """Called when a custom msg is received."""
374 self._msg_callbacks(self, content)
397 self._msg_callbacks(self, content, buffers)
375
398
376 def _notify_trait(self, name, old_value, new_value):
399 def _notify_trait(self, name, old_value, new_value):
377 """Called when a property has been changed."""
400 """Called when a property has been changed."""
378 # Trigger default traitlet callback machinery. This allows any user
401 # Trigger default traitlet callback machinery. This allows any user
379 # registered validation to be processed prior to allowing the widget
402 # registered validation to be processed prior to allowing the widget
380 # machinery to handle the state.
403 # machinery to handle the state.
381 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
404 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
382
405
383 # Send the state after the user registered callbacks for trait changes
406 # Send the state after the user registered callbacks for trait changes
384 # have all fired (allows for user to validate values).
407 # have all fired (allows for user to validate values).
385 if self.comm is not None and name in self.keys:
408 if self.comm is not None and name in self.keys:
386 # Make sure this isn't information that the front-end just sent us.
409 # Make sure this isn't information that the front-end just sent us.
387 if self._should_send_property(name, new_value):
410 if self._should_send_property(name, new_value):
388 # Send new state to front-end
411 # Send new state to front-end
389 self.send_state(key=name)
412 self.send_state(key=name)
390
413
391 def _handle_displayed(self, **kwargs):
414 def _handle_displayed(self, **kwargs):
392 """Called when a view has been displayed for this widget instance"""
415 """Called when a view has been displayed for this widget instance"""
393 self._display_callbacks(self, **kwargs)
416 self._display_callbacks(self, **kwargs)
394
417
395 def _trait_to_json(self, x):
418 def _trait_to_json(self, x):
396 """Convert a trait value to json
419 """Convert a trait value to json."""
397
420 return x
398 Traverse lists/tuples and dicts and serialize their values as well.
399 Replace any widgets with their model_id
400 """
401 if isinstance(x, dict):
402 return {k: self._trait_to_json(v) for k, v in x.items()}
403 elif isinstance(x, (list, tuple)):
404 return [self._trait_to_json(v) for v in x]
405 elif isinstance(x, Widget):
406 return "IPY_MODEL_" + x.model_id
407 else:
408 return x # Value must be JSON-able
409
421
410 def _trait_from_json(self, x):
422 def _trait_from_json(self, x):
411 """Convert json values to objects
423 """Convert json values to objects."""
412
424 return x
413 Replace any strings representing valid model id values to Widget references.
414 """
415 if isinstance(x, dict):
416 return {k: self._trait_from_json(v) for k, v in x.items()}
417 elif isinstance(x, (list, tuple)):
418 return [self._trait_from_json(v) for v in x]
419 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
420 # we want to support having child widgets at any level in a hierarchy
421 # trusting that a widget UUID will not appear out in the wild
422 return Widget.widgets[x[10:]]
423 else:
424 return x
425
425
426 def _ipython_display_(self, **kwargs):
426 def _ipython_display_(self, **kwargs):
427 """Called when `IPython.display.display` is called on the widget."""
427 """Called when `IPython.display.display` is called on the widget."""
428 # Show view.
428 # Show view.
429 if self._view_name is not None:
429 if self._view_name is not None:
430 self._send({"method": "display"})
430 self._send({"method": "display"})
431 self._handle_displayed(**kwargs)
431 self._handle_displayed(**kwargs)
432
432
433 def _send(self, msg):
433 def _send(self, msg, buffers=None):
434 """Sends a message to the model in the front-end."""
434 """Sends a message to the model in the front-end."""
435 self.comm.send(msg)
435 self.comm.send(data=msg, buffers=buffers)
436
436
437
437
438 class DOMWidget(Widget):
438 class DOMWidget(Widget):
439 visible = Bool(True, allow_none=True, help="Whether the widget is visible. False collapses the empty space, while None preserves the empty space.", sync=True)
439 visible = Bool(True, allow_none=True, help="Whether the widget is visible. False collapses the empty space, while None preserves the empty space.", sync=True)
440 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
440 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
441 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
441 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
442
442
443 width = CUnicode(sync=True)
443 width = CUnicode(sync=True)
444 height = CUnicode(sync=True)
444 height = CUnicode(sync=True)
445 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
445 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
446 padding = CUnicode(sync=True)
446 padding = CUnicode(sync=True)
447 margin = CUnicode(sync=True)
447 margin = CUnicode(sync=True)
448
448
449 color = Color(None, allow_none=True, sync=True)
449 color = Color(None, allow_none=True, sync=True)
450 background_color = Color(None, allow_none=True, sync=True)
450 background_color = Color(None, allow_none=True, sync=True)
451 border_color = Color(None, allow_none=True, sync=True)
451 border_color = Color(None, allow_none=True, sync=True)
452
452
453 border_width = CUnicode(sync=True)
453 border_width = CUnicode(sync=True)
454 border_radius = CUnicode(sync=True)
454 border_radius = CUnicode(sync=True)
455 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
455 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
456 'none',
456 'none',
457 'hidden',
457 'hidden',
458 'dotted',
458 'dotted',
459 'dashed',
459 'dashed',
460 'solid',
460 'solid',
461 'double',
461 'double',
462 'groove',
462 'groove',
463 'ridge',
463 'ridge',
464 'inset',
464 'inset',
465 'outset',
465 'outset',
466 'initial',
466 'initial',
467 'inherit', ''],
467 'inherit', ''],
468 default_value='', sync=True)
468 default_value='', sync=True)
469
469
470 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
470 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
471 'normal',
471 'normal',
472 'italic',
472 'italic',
473 'oblique',
473 'oblique',
474 'initial',
474 'initial',
475 'inherit', ''],
475 'inherit', ''],
476 default_value='', sync=True)
476 default_value='', sync=True)
477 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
477 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
478 'normal',
478 'normal',
479 'bold',
479 'bold',
480 'bolder',
480 'bolder',
481 'lighter',
481 'lighter',
482 'initial',
482 'initial',
483 'inherit', ''] + list(map(str, range(100,1000,100))),
483 'inherit', ''] + list(map(str, range(100,1000,100))),
484 default_value='', sync=True)
484 default_value='', sync=True)
485 font_size = CUnicode(sync=True)
485 font_size = CUnicode(sync=True)
486 font_family = Unicode(sync=True)
486 font_family = Unicode(sync=True)
487
487
488 def __init__(self, *pargs, **kwargs):
488 def __init__(self, *pargs, **kwargs):
489 super(DOMWidget, self).__init__(*pargs, **kwargs)
489 super(DOMWidget, self).__init__(*pargs, **kwargs)
490
490
491 def _validate_border(name, old, new):
491 def _validate_border(name, old, new):
492 if new is not None and new != '':
492 if new is not None and new != '':
493 if name != 'border_width' and not self.border_width:
493 if name != 'border_width' and not self.border_width:
494 self.border_width = 1
494 self.border_width = 1
495 if name != 'border_style' and self.border_style == '':
495 if name != 'border_style' and self.border_style == '':
496 self.border_style = 'solid'
496 self.border_style = 'solid'
497 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
497 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,80 +1,107 b''
1 """Box class.
1 """Box class.
2
2
3 Represents a container that can be used to group other widgets.
3 Represents a container that can be used to group other widgets.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from .widget import DOMWidget, register
9 from .widget import DOMWidget, Widget, register
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 from IPython.utils.warn import DeprecatedClass
11 from IPython.utils.warn import DeprecatedClass
12
12
13 def _widget_to_json(x):
14 if isinstance(x, dict):
15 return {k: _widget_to_json(v) for k, v in x.items()}
16 elif isinstance(x, (list, tuple)):
17 return [_widget_to_json(v) for v in x]
18 elif isinstance(x, Widget):
19 return "IPY_MODEL_" + x.model_id
20 else:
21 return x
22
23 def _json_to_widget(x):
24 if isinstance(x, dict):
25 return {k: _json_to_widget(v) for k, v in x.items()}
26 elif isinstance(x, (list, tuple)):
27 return [_json_to_widget(v) for v in x]
28 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
29 return Widget.widgets[x[10:]]
30 else:
31 return x
32
33 widget_serialization = {
34 'from_json': _json_to_widget,
35 'to_json': _widget_to_json
36 }
37
38
13 @register('IPython.Box')
39 @register('IPython.Box')
14 class Box(DOMWidget):
40 class Box(DOMWidget):
15 """Displays multiple widgets in a group."""
41 """Displays multiple widgets in a group."""
42 _model_name = Unicode('BoxModel', sync=True)
16 _view_name = Unicode('BoxView', sync=True)
43 _view_name = Unicode('BoxView', sync=True)
17
44
18 # Child widgets in the container.
45 # Child widgets in the container.
19 # Using a tuple here to force reassignment to update the list.
46 # Using a tuple here to force reassignment to update the list.
20 # When a proper notifying-list trait exists, that is what should be used here.
47 # When a proper notifying-list trait exists, that is what should be used here.
21 children = Tuple(sync=True)
48 children = Tuple(sync=True, **widget_serialization)
22
49
23 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
50 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
24 overflow_x = CaselessStrEnum(
51 overflow_x = CaselessStrEnum(
25 values=_overflow_values,
52 values=_overflow_values,
26 default_value='', sync=True, help="""Specifies what
53 default_value='', sync=True, help="""Specifies what
27 happens to content that is too large for the rendered region.""")
54 happens to content that is too large for the rendered region.""")
28 overflow_y = CaselessStrEnum(
55 overflow_y = CaselessStrEnum(
29 values=_overflow_values,
56 values=_overflow_values,
30 default_value='', sync=True, help="""Specifies what
57 default_value='', sync=True, help="""Specifies what
31 happens to content that is too large for the rendered region.""")
58 happens to content that is too large for the rendered region.""")
32
59
33 box_style = CaselessStrEnum(
60 box_style = CaselessStrEnum(
34 values=['success', 'info', 'warning', 'danger', ''],
61 values=['success', 'info', 'warning', 'danger', ''],
35 default_value='', allow_none=True, sync=True, help="""Use a
62 default_value='', allow_none=True, sync=True, help="""Use a
36 predefined styling for the box.""")
63 predefined styling for the box.""")
37
64
38 def __init__(self, children = (), **kwargs):
65 def __init__(self, children = (), **kwargs):
39 kwargs['children'] = children
66 kwargs['children'] = children
40 super(Box, self).__init__(**kwargs)
67 super(Box, self).__init__(**kwargs)
41 self.on_displayed(Box._fire_children_displayed)
68 self.on_displayed(Box._fire_children_displayed)
42
69
43 def _fire_children_displayed(self):
70 def _fire_children_displayed(self):
44 for child in self.children:
71 for child in self.children:
45 child._handle_displayed()
72 child._handle_displayed()
46
73
47
74
48 @register('IPython.FlexBox')
75 @register('IPython.FlexBox')
49 class FlexBox(Box):
76 class FlexBox(Box):
50 """Displays multiple widgets using the flexible box model."""
77 """Displays multiple widgets using the flexible box model."""
51 _view_name = Unicode('FlexBoxView', sync=True)
78 _view_name = Unicode('FlexBoxView', sync=True)
52 orientation = CaselessStrEnum(values=['vertical', 'horizontal'], default_value='vertical', sync=True)
79 orientation = CaselessStrEnum(values=['vertical', 'horizontal'], default_value='vertical', sync=True)
53 flex = Int(0, sync=True, help="""Specify the flexible-ness of the model.""")
80 flex = Int(0, sync=True, help="""Specify the flexible-ness of the model.""")
54 def _flex_changed(self, name, old, new):
81 def _flex_changed(self, name, old, new):
55 new = min(max(0, new), 2)
82 new = min(max(0, new), 2)
56 if self.flex != new:
83 if self.flex != new:
57 self.flex = new
84 self.flex = new
58
85
59 _locations = ['start', 'center', 'end', 'baseline', 'stretch']
86 _locations = ['start', 'center', 'end', 'baseline', 'stretch']
60 pack = CaselessStrEnum(
87 pack = CaselessStrEnum(
61 values=_locations,
88 values=_locations,
62 default_value='start', sync=True)
89 default_value='start', sync=True)
63 align = CaselessStrEnum(
90 align = CaselessStrEnum(
64 values=_locations,
91 values=_locations,
65 default_value='start', sync=True)
92 default_value='start', sync=True)
66
93
67
94
68 def VBox(*pargs, **kwargs):
95 def VBox(*pargs, **kwargs):
69 """Displays multiple widgets vertically using the flexible box model."""
96 """Displays multiple widgets vertically using the flexible box model."""
70 kwargs['orientation'] = 'vertical'
97 kwargs['orientation'] = 'vertical'
71 return FlexBox(*pargs, **kwargs)
98 return FlexBox(*pargs, **kwargs)
72
99
73 def HBox(*pargs, **kwargs):
100 def HBox(*pargs, **kwargs):
74 """Displays multiple widgets horizontally using the flexible box model."""
101 """Displays multiple widgets horizontally using the flexible box model."""
75 kwargs['orientation'] = 'horizontal'
102 kwargs['orientation'] = 'horizontal'
76 return FlexBox(*pargs, **kwargs)
103 return FlexBox(*pargs, **kwargs)
77
104
78
105
79 # Remove in IPython 4.0
106 # Remove in IPython 4.0
80 ContainerWidget = DeprecatedClass(Box, 'ContainerWidget')
107 ContainerWidget = DeprecatedClass(Box, 'ContainerWidget')
@@ -1,82 +1,82 b''
1 """Button class.
1 """Button class.
2
2
3 Represents a button in the frontend using a widget. Allows user to listen for
3 Represents a button in the frontend using a widget. Allows user to listen for
4 click events on the button and trigger backend code when the clicks are fired.
4 click events on the button and trigger backend code when the clicks are fired.
5 """
5 """
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Copyright (c) 2013, the IPython Development Team.
7 # Copyright (c) 2013, the IPython Development Team.
8 #
8 #
9 # Distributed under the terms of the Modified BSD License.
9 # Distributed under the terms of the Modified BSD License.
10 #
10 #
11 # The full license is in the file COPYING.txt, distributed with this software.
11 # The full license is in the file COPYING.txt, distributed with this software.
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 # Imports
15 # Imports
16 #-----------------------------------------------------------------------------
16 #-----------------------------------------------------------------------------
17 from .widget import DOMWidget, CallbackDispatcher, register
17 from .widget import DOMWidget, CallbackDispatcher, register
18 from IPython.utils.traitlets import Unicode, Bool, CaselessStrEnum
18 from IPython.utils.traitlets import Unicode, Bool, CaselessStrEnum
19 from IPython.utils.warn import DeprecatedClass
19 from IPython.utils.warn import DeprecatedClass
20
20
21 #-----------------------------------------------------------------------------
21 #-----------------------------------------------------------------------------
22 # Classes
22 # Classes
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 @register('IPython.Button')
24 @register('IPython.Button')
25 class Button(DOMWidget):
25 class Button(DOMWidget):
26 """Button widget.
26 """Button widget.
27 This widget has an `on_click` method that allows you to listen for the
27 This widget has an `on_click` method that allows you to listen for the
28 user clicking on the button. The click event itself is stateless.
28 user clicking on the button. The click event itself is stateless.
29
29
30 Parameters
30 Parameters
31 ----------
31 ----------
32 description : str
32 description : str
33 description displayed next to the button
33 description displayed next to the button
34 tooltip: str
34 tooltip: str
35 tooltip caption of the toggle button
35 tooltip caption of the toggle button
36 icon: str
36 icon: str
37 font-awesome icon name
37 font-awesome icon name
38 """
38 """
39 _view_name = Unicode('ButtonView', sync=True)
39 _view_name = Unicode('ButtonView', sync=True)
40
40
41 # Keys
41 # Keys
42 description = Unicode('', help="Button label.", sync=True)
42 description = Unicode('', help="Button label.", sync=True)
43 tooltip = Unicode(help="Tooltip caption of the button.", sync=True)
43 tooltip = Unicode(help="Tooltip caption of the button.", sync=True)
44 disabled = Bool(False, help="Enable or disable user changes.", sync=True)
44 disabled = Bool(False, help="Enable or disable user changes.", sync=True)
45 icon = Unicode('', help= "Font-awesome icon.", sync=True)
45 icon = Unicode('', help= "Font-awesome icon.", sync=True)
46
46
47 button_style = CaselessStrEnum(
47 button_style = CaselessStrEnum(
48 values=['primary', 'success', 'info', 'warning', 'danger', ''],
48 values=['primary', 'success', 'info', 'warning', 'danger', ''],
49 default_value='', allow_none=True, sync=True, help="""Use a
49 default_value='', allow_none=True, sync=True, help="""Use a
50 predefined styling for the button.""")
50 predefined styling for the button.""")
51
51
52 def __init__(self, **kwargs):
52 def __init__(self, **kwargs):
53 """Constructor"""
53 """Constructor"""
54 super(Button, self).__init__(**kwargs)
54 super(Button, self).__init__(**kwargs)
55 self._click_handlers = CallbackDispatcher()
55 self._click_handlers = CallbackDispatcher()
56 self.on_msg(self._handle_button_msg)
56 self.on_msg(self._handle_button_msg)
57
57
58 def on_click(self, callback, remove=False):
58 def on_click(self, callback, remove=False):
59 """Register a callback to execute when the button is clicked.
59 """Register a callback to execute when the button is clicked.
60
60
61 The callback will be called with one argument,
61 The callback will be called with one argument,
62 the clicked button widget instance.
62 the clicked button widget instance.
63
63
64 Parameters
64 Parameters
65 ----------
65 ----------
66 remove : bool (optional)
66 remove : bool (optional)
67 Set to true to remove the callback from the list of callbacks."""
67 Set to true to remove the callback from the list of callbacks."""
68 self._click_handlers.register_callback(callback, remove=remove)
68 self._click_handlers.register_callback(callback, remove=remove)
69
69
70 def _handle_button_msg(self, _, content):
70 def _handle_button_msg(self, _, content, buffers):
71 """Handle a msg from the front-end.
71 """Handle a msg from the front-end.
72
72
73 Parameters
73 Parameters
74 ----------
74 ----------
75 content: dict
75 content: dict
76 Content of the msg."""
76 Content of the msg."""
77 if content.get('event', '') == 'click':
77 if content.get('event', '') == 'click':
78 self._click_handlers(self)
78 self._click_handlers(self)
79
79
80
80
81 # Remove in IPython 4.0
81 # Remove in IPython 4.0
82 ButtonWidget = DeprecatedClass(Button, 'ButtonWidget')
82 ButtonWidget = DeprecatedClass(Button, 'ButtonWidget')
General Comments 0
You need to be logged in to leave comments. Login now