##// END OF EJS Templates
Merge pull request #7097 from jasongrout/widget-visibility...
Jonathan Frederic -
r19417:6daf9b39 merge
parent child Browse files
Show More
@@ -1,735 +1,742 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
11
12 var WidgetModel = Backbone.Model.extend({
12 var WidgetModel = Backbone.Model.extend({
13 constructor: function (widget_manager, model_id, comm) {
13 constructor: function (widget_manager, model_id, comm) {
14 /**
14 /**
15 * Constructor
15 * Constructor
16 *
16 *
17 * Creates a WidgetModel instance.
17 * Creates a WidgetModel instance.
18 *
18 *
19 * Parameters
19 * Parameters
20 * ----------
20 * ----------
21 * widget_manager : WidgetManager instance
21 * widget_manager : WidgetManager instance
22 * model_id : string
22 * model_id : string
23 * An ID unique to this model.
23 * An ID unique to this model.
24 * comm : Comm instance (optional)
24 * comm : Comm instance (optional)
25 */
25 */
26 this.widget_manager = widget_manager;
26 this.widget_manager = widget_manager;
27 this.state_change = Promise.resolve();
27 this.state_change = Promise.resolve();
28 this._buffered_state_diff = {};
28 this._buffered_state_diff = {};
29 this.pending_msgs = 0;
29 this.pending_msgs = 0;
30 this.msg_buffer = null;
30 this.msg_buffer = null;
31 this.state_lock = null;
31 this.state_lock = null;
32 this.id = model_id;
32 this.id = model_id;
33 this.views = {};
33 this.views = {};
34 this._resolve_received_state = {};
34 this._resolve_received_state = {};
35
35
36 if (comm !== undefined) {
36 if (comm !== undefined) {
37 // Remember comm associated with the model.
37 // Remember comm associated with the model.
38 this.comm = comm;
38 this.comm = comm;
39 comm.model = this;
39 comm.model = this;
40
40
41 // Hook comm messages up to model.
41 // Hook comm messages up to model.
42 comm.on_close($.proxy(this._handle_comm_closed, this));
42 comm.on_close($.proxy(this._handle_comm_closed, this));
43 comm.on_msg($.proxy(this._handle_comm_msg, this));
43 comm.on_msg($.proxy(this._handle_comm_msg, this));
44
44
45 // Assume the comm is alive.
45 // Assume the comm is alive.
46 this.set_comm_live(true);
46 this.set_comm_live(true);
47 } else {
47 } else {
48 this.set_comm_live(false);
48 this.set_comm_live(false);
49 }
49 }
50 return Backbone.Model.apply(this);
50 return Backbone.Model.apply(this);
51 },
51 },
52
52
53 send: function (content, callbacks) {
53 send: function (content, callbacks) {
54 /**
54 /**
55 * Send a custom msg over the comm.
55 * Send a custom msg over the comm.
56 */
56 */
57 if (this.comm !== undefined) {
57 if (this.comm !== undefined) {
58 var data = {method: 'custom', content: content};
58 var data = {method: 'custom', content: content};
59 this.comm.send(data, callbacks);
59 this.comm.send(data, callbacks);
60 this.pending_msgs++;
60 this.pending_msgs++;
61 }
61 }
62 },
62 },
63
63
64 request_state: function(callbacks) {
64 request_state: function(callbacks) {
65 /**
65 /**
66 * Request a state push from the back-end.
66 * Request a state push from the back-end.
67 */
67 */
68 if (!this.comm) {
68 if (!this.comm) {
69 console.error("Could not request_state because comm doesn't exist!");
69 console.error("Could not request_state because comm doesn't exist!");
70 return;
70 return;
71 }
71 }
72
72
73 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
73 var msg_id = this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
74
74
75 // Promise that is resolved when a state is received
75 // Promise that is resolved when a state is received
76 // from the back-end.
76 // from the back-end.
77 var that = this;
77 var that = this;
78 var received_state = new Promise(function(resolve) {
78 var received_state = new Promise(function(resolve) {
79 that._resolve_received_state[msg_id] = resolve;
79 that._resolve_received_state[msg_id] = resolve;
80 });
80 });
81 return received_state;
81 return received_state;
82 },
82 },
83
83
84 set_comm_live: function(live) {
84 set_comm_live: function(live) {
85 /**
85 /**
86 * Change the comm_live state of the model.
86 * Change the comm_live state of the model.
87 */
87 */
88 if (this.comm_live === undefined || this.comm_live != live) {
88 if (this.comm_live === undefined || this.comm_live != live) {
89 this.comm_live = live;
89 this.comm_live = live;
90 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
90 this.trigger(live ? 'comm:live' : 'comm:dead', {model: this});
91 }
91 }
92 },
92 },
93
93
94 close: function(comm_closed) {
94 close: function(comm_closed) {
95 /**
95 /**
96 * Close model
96 * Close model
97 */
97 */
98 if (this.comm && !comm_closed) {
98 if (this.comm && !comm_closed) {
99 this.comm.close();
99 this.comm.close();
100 }
100 }
101 this.stopListening();
101 this.stopListening();
102 this.trigger('destroy', this);
102 this.trigger('destroy', this);
103 delete this.comm.model; // Delete ref so GC will collect widget model.
103 delete this.comm.model; // Delete ref so GC will collect widget model.
104 delete this.comm;
104 delete this.comm;
105 delete this.model_id; // Delete id from model so widget manager cleans up.
105 delete this.model_id; // Delete id from model so widget manager cleans up.
106 _.each(this.views, function(v, id, views) {
106 _.each(this.views, function(v, id, views) {
107 v.then(function(view) {
107 v.then(function(view) {
108 view.remove();
108 view.remove();
109 delete views[id];
109 delete views[id];
110 });
110 });
111 });
111 });
112 },
112 },
113
113
114 _handle_comm_closed: function (msg) {
114 _handle_comm_closed: function (msg) {
115 /**
115 /**
116 * Handle when a widget is closed.
116 * Handle when a widget is closed.
117 */
117 */
118 this.trigger('comm:close');
118 this.trigger('comm:close');
119 this.close(true);
119 this.close(true);
120 },
120 },
121
121
122 _handle_comm_msg: function (msg) {
122 _handle_comm_msg: function (msg) {
123 /**
123 /**
124 * Handle incoming comm msg.
124 * Handle incoming comm msg.
125 */
125 */
126 var method = msg.content.data.method;
126 var method = msg.content.data.method;
127 var that = this;
127 var that = this;
128 switch (method) {
128 switch (method) {
129 case 'update':
129 case 'update':
130 this.state_change = this.state_change
130 this.state_change = this.state_change
131 .then(function() {
131 .then(function() {
132 return that.set_state(msg.content.data.state);
132 return that.set_state(msg.content.data.state);
133 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
133 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true))
134 .then(function() {
134 .then(function() {
135 var parent_id = msg.parent_header.msg_id;
135 var parent_id = msg.parent_header.msg_id;
136 if (that._resolve_received_state[parent_id] !== undefined) {
136 if (that._resolve_received_state[parent_id] !== undefined) {
137 that._resolve_received_state[parent_id].call();
137 that._resolve_received_state[parent_id].call();
138 delete that._resolve_received_state[parent_id];
138 delete that._resolve_received_state[parent_id];
139 }
139 }
140 }).catch(utils.reject("Couldn't resolve state request promise", true));
140 }).catch(utils.reject("Couldn't resolve state request promise", true));
141 break;
141 break;
142 case 'custom':
142 case 'custom':
143 this.trigger('msg:custom', msg.content.data.content);
143 this.trigger('msg:custom', msg.content.data.content);
144 break;
144 break;
145 case 'display':
145 case 'display':
146 this.widget_manager.display_view(msg, this)
146 this.widget_manager.display_view(msg, this)
147 .catch(utils.reject('Could not process display view msg', true));
147 .catch(utils.reject('Could not process display view msg', true));
148 break;
148 break;
149 }
149 }
150 },
150 },
151
151
152 set_state: function (state) {
152 set_state: function (state) {
153 var that = this;
153 var that = this;
154 // Handle when a widget is updated via the python side.
154 // Handle when a widget is updated via the python side.
155 return this._unpack_models(state).then(function(state) {
155 return this._unpack_models(state).then(function(state) {
156 that.state_lock = state;
156 that.state_lock = state;
157 try {
157 try {
158 WidgetModel.__super__.set.call(that, state);
158 WidgetModel.__super__.set.call(that, state);
159 } finally {
159 } finally {
160 that.state_lock = null;
160 that.state_lock = null;
161 }
161 }
162 }).catch(utils.reject("Couldn't set model state", true));
162 }).catch(utils.reject("Couldn't set model state", true));
163 },
163 },
164
164
165 get_state: function() {
165 get_state: function() {
166 // Get the serializable state of the model.
166 // Get the serializable state of the model.
167 state = this.toJSON();
167 state = this.toJSON();
168 for (var key in state) {
168 for (var key in state) {
169 if (state.hasOwnProperty(key)) {
169 if (state.hasOwnProperty(key)) {
170 state[key] = this._pack_models(state[key]);
170 state[key] = this._pack_models(state[key]);
171 }
171 }
172 }
172 }
173 return state;
173 return state;
174 },
174 },
175
175
176 _handle_status: function (msg, callbacks) {
176 _handle_status: function (msg, callbacks) {
177 /**
177 /**
178 * Handle status msgs.
178 * Handle status msgs.
179 *
179 *
180 * execution_state : ('busy', 'idle', 'starting')
180 * execution_state : ('busy', 'idle', 'starting')
181 */
181 */
182 if (this.comm !== undefined) {
182 if (this.comm !== undefined) {
183 if (msg.content.execution_state ==='idle') {
183 if (msg.content.execution_state ==='idle') {
184 // Send buffer if this message caused another message to be
184 // Send buffer if this message caused another message to be
185 // throttled.
185 // throttled.
186 if (this.msg_buffer !== null &&
186 if (this.msg_buffer !== null &&
187 (this.get('msg_throttle') || 3) === this.pending_msgs) {
187 (this.get('msg_throttle') || 3) === this.pending_msgs) {
188 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
188 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
189 this.comm.send(data, callbacks);
189 this.comm.send(data, callbacks);
190 this.msg_buffer = null;
190 this.msg_buffer = null;
191 } else {
191 } else {
192 --this.pending_msgs;
192 --this.pending_msgs;
193 }
193 }
194 }
194 }
195 }
195 }
196 },
196 },
197
197
198 callbacks: function(view) {
198 callbacks: function(view) {
199 /**
199 /**
200 * Create msg callbacks for a comm msg.
200 * Create msg callbacks for a comm msg.
201 */
201 */
202 var callbacks = this.widget_manager.callbacks(view);
202 var callbacks = this.widget_manager.callbacks(view);
203
203
204 if (callbacks.iopub === undefined) {
204 if (callbacks.iopub === undefined) {
205 callbacks.iopub = {};
205 callbacks.iopub = {};
206 }
206 }
207
207
208 var that = this;
208 var that = this;
209 callbacks.iopub.status = function (msg) {
209 callbacks.iopub.status = function (msg) {
210 that._handle_status(msg, callbacks);
210 that._handle_status(msg, callbacks);
211 };
211 };
212 return callbacks;
212 return callbacks;
213 },
213 },
214
214
215 set: function(key, val, options) {
215 set: function(key, val, options) {
216 /**
216 /**
217 * Set a value.
217 * Set a value.
218 */
218 */
219 var return_value = WidgetModel.__super__.set.apply(this, arguments);
219 var return_value = WidgetModel.__super__.set.apply(this, arguments);
220
220
221 // Backbone only remembers the diff of the most recent set()
221 // Backbone only remembers the diff of the most recent set()
222 // operation. Calling set multiple times in a row results in a
222 // operation. Calling set multiple times in a row results in a
223 // loss of diff information. Here we keep our own running diff.
223 // loss of diff information. Here we keep our own running diff.
224 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
224 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
225 return return_value;
225 return return_value;
226 },
226 },
227
227
228 sync: function (method, model, options) {
228 sync: function (method, model, options) {
229 /**
229 /**
230 * Handle sync to the back-end. Called when a model.save() is called.
230 * Handle sync to the back-end. Called when a model.save() is called.
231 *
231 *
232 * Make sure a comm exists.
232 * Make sure a comm exists.
233 */
233 */
234 var error = options.error || function() {
234 var error = options.error || function() {
235 console.error('Backbone sync error:', arguments);
235 console.error('Backbone sync error:', arguments);
236 };
236 };
237 if (this.comm === undefined) {
237 if (this.comm === undefined) {
238 error();
238 error();
239 return false;
239 return false;
240 }
240 }
241
241
242 // Delete any key value pairs that the back-end already knows about.
242 // Delete any key value pairs that the back-end already knows about.
243 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
243 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
244 if (this.state_lock !== null) {
244 if (this.state_lock !== null) {
245 var keys = Object.keys(this.state_lock);
245 var keys = Object.keys(this.state_lock);
246 for (var i=0; i<keys.length; i++) {
246 for (var i=0; i<keys.length; i++) {
247 var key = keys[i];
247 var key = keys[i];
248 if (attrs[key] === this.state_lock[key]) {
248 if (attrs[key] === this.state_lock[key]) {
249 delete attrs[key];
249 delete attrs[key];
250 }
250 }
251 }
251 }
252 }
252 }
253
253
254 // Only sync if there are attributes to send to the back-end.
254 // Only sync if there are attributes to send to the back-end.
255 attrs = this._pack_models(attrs);
255 attrs = this._pack_models(attrs);
256 if (_.size(attrs) > 0) {
256 if (_.size(attrs) > 0) {
257
257
258 // If this message was sent via backbone itself, it will not
258 // If this message was sent via backbone itself, it will not
259 // have any callbacks. It's important that we create callbacks
259 // have any callbacks. It's important that we create callbacks
260 // so we can listen for status messages, etc...
260 // so we can listen for status messages, etc...
261 var callbacks = options.callbacks || this.callbacks();
261 var callbacks = options.callbacks || this.callbacks();
262
262
263 // Check throttle.
263 // Check throttle.
264 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
264 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
265 // The throttle has been exceeded, buffer the current msg so
265 // The throttle has been exceeded, buffer the current msg so
266 // it can be sent once the kernel has finished processing
266 // it can be sent once the kernel has finished processing
267 // some of the existing messages.
267 // some of the existing messages.
268
268
269 // Combine updates if it is a 'patch' sync, otherwise replace updates
269 // Combine updates if it is a 'patch' sync, otherwise replace updates
270 switch (method) {
270 switch (method) {
271 case 'patch':
271 case 'patch':
272 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
272 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
273 break;
273 break;
274 case 'update':
274 case 'update':
275 case 'create':
275 case 'create':
276 this.msg_buffer = attrs;
276 this.msg_buffer = attrs;
277 break;
277 break;
278 default:
278 default:
279 error();
279 error();
280 return false;
280 return false;
281 }
281 }
282 this.msg_buffer_callbacks = callbacks;
282 this.msg_buffer_callbacks = callbacks;
283
283
284 } else {
284 } else {
285 // We haven't exceeded the throttle, send the message like
285 // We haven't exceeded the throttle, send the message like
286 // normal.
286 // normal.
287 var data = {method: 'backbone', sync_data: attrs};
287 var data = {method: 'backbone', sync_data: attrs};
288 this.comm.send(data, callbacks);
288 this.comm.send(data, callbacks);
289 this.pending_msgs++;
289 this.pending_msgs++;
290 }
290 }
291 }
291 }
292 // Since the comm is a one-way communication, assume the message
292 // Since the comm is a one-way communication, assume the message
293 // arrived. Don't call success since we don't have a model back from the server
293 // arrived. Don't call success since we don't have a model back from the server
294 // this means we miss out on the 'sync' event.
294 // this means we miss out on the 'sync' event.
295 this._buffered_state_diff = {};
295 this._buffered_state_diff = {};
296 },
296 },
297
297
298 save_changes: function(callbacks) {
298 save_changes: function(callbacks) {
299 /**
299 /**
300 * Push this model's state to the back-end
300 * Push this model's state to the back-end
301 *
301 *
302 * This invokes a Backbone.Sync.
302 * This invokes a Backbone.Sync.
303 */
303 */
304 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
304 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
305 },
305 },
306
306
307 _pack_models: function(value) {
307 _pack_models: function(value) {
308 /**
308 /**
309 * Replace models with model ids recursively.
309 * Replace models with model ids recursively.
310 */
310 */
311 var that = this;
311 var that = this;
312 var packed;
312 var packed;
313 if (value instanceof Backbone.Model) {
313 if (value instanceof Backbone.Model) {
314 return "IPY_MODEL_" + value.id;
314 return "IPY_MODEL_" + value.id;
315
315
316 } else if ($.isArray(value)) {
316 } else if ($.isArray(value)) {
317 packed = [];
317 packed = [];
318 _.each(value, function(sub_value, key) {
318 _.each(value, function(sub_value, key) {
319 packed.push(that._pack_models(sub_value));
319 packed.push(that._pack_models(sub_value));
320 });
320 });
321 return packed;
321 return packed;
322 } else if (value instanceof Date || value instanceof String) {
322 } else if (value instanceof Date || value instanceof String) {
323 return value;
323 return value;
324 } else if (value instanceof Object) {
324 } else if (value instanceof Object) {
325 packed = {};
325 packed = {};
326 _.each(value, function(sub_value, key) {
326 _.each(value, function(sub_value, key) {
327 packed[key] = that._pack_models(sub_value);
327 packed[key] = that._pack_models(sub_value);
328 });
328 });
329 return packed;
329 return packed;
330
330
331 } else {
331 } else {
332 return value;
332 return value;
333 }
333 }
334 },
334 },
335
335
336 _unpack_models: function(value) {
336 _unpack_models: function(value) {
337 /**
337 /**
338 * Replace model ids with models recursively.
338 * Replace model ids with models recursively.
339 */
339 */
340 var that = this;
340 var that = this;
341 var unpacked;
341 var unpacked;
342 if ($.isArray(value)) {
342 if ($.isArray(value)) {
343 unpacked = [];
343 unpacked = [];
344 _.each(value, function(sub_value, key) {
344 _.each(value, function(sub_value, key) {
345 unpacked.push(that._unpack_models(sub_value));
345 unpacked.push(that._unpack_models(sub_value));
346 });
346 });
347 return Promise.all(unpacked);
347 return Promise.all(unpacked);
348 } else if (value instanceof Object) {
348 } else if (value instanceof Object) {
349 unpacked = {};
349 unpacked = {};
350 _.each(value, function(sub_value, key) {
350 _.each(value, function(sub_value, key) {
351 unpacked[key] = that._unpack_models(sub_value);
351 unpacked[key] = that._unpack_models(sub_value);
352 });
352 });
353 return utils.resolve_promises_dict(unpacked);
353 return utils.resolve_promises_dict(unpacked);
354 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
354 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
355 // get_model returns a promise already
355 // get_model returns a promise already
356 return this.widget_manager.get_model(value.slice(10, value.length));
356 return this.widget_manager.get_model(value.slice(10, value.length));
357 } else {
357 } else {
358 return Promise.resolve(value);
358 return Promise.resolve(value);
359 }
359 }
360 },
360 },
361
361
362 on_some_change: function(keys, callback, context) {
362 on_some_change: function(keys, callback, context) {
363 /**
363 /**
364 * on_some_change(["key1", "key2"], foo, context) differs from
364 * on_some_change(["key1", "key2"], foo, context) differs from
365 * on("change:key1 change:key2", foo, context).
365 * on("change:key1 change:key2", foo, context).
366 * If the widget attributes key1 and key2 are both modified,
366 * If the widget attributes key1 and key2 are both modified,
367 * the second form will result in foo being called twice
367 * the second form will result in foo being called twice
368 * while the first will call foo only once.
368 * while the first will call foo only once.
369 */
369 */
370 this.on('change', function() {
370 this.on('change', function() {
371 if (keys.some(this.hasChanged, this)) {
371 if (keys.some(this.hasChanged, this)) {
372 callback.apply(context);
372 callback.apply(context);
373 }
373 }
374 }, this);
374 }, this);
375
375
376 },
376 },
377 });
377 });
378 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
378 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
379
379
380
380
381 var WidgetView = Backbone.View.extend({
381 var WidgetView = Backbone.View.extend({
382 initialize: function(parameters) {
382 initialize: function(parameters) {
383 /**
383 /**
384 * Public constructor.
384 * Public constructor.
385 */
385 */
386 this.model.on('change',this.update,this);
386 this.model.on('change',this.update,this);
387 this.options = parameters.options;
387 this.options = parameters.options;
388 this.on('displayed', function() {
388 this.on('displayed', function() {
389 this.is_displayed = true;
389 this.is_displayed = true;
390 }, this);
390 }, this);
391 },
391 },
392
392
393 update: function(){
393 update: function(){
394 /**
394 /**
395 * Triggered on model change.
395 * Triggered on model change.
396 *
396 *
397 * Update view to be consistent with this.model
397 * Update view to be consistent with this.model
398 */
398 */
399 },
399 },
400
400
401 create_child_view: function(child_model, options) {
401 create_child_view: function(child_model, options) {
402 /**
402 /**
403 * Create and promise that resolves to a child view of a given model
403 * Create and promise that resolves to a child view of a given model
404 */
404 */
405 var that = this;
405 var that = this;
406 options = $.extend({ parent: this }, options || {});
406 options = $.extend({ parent: this }, options || {});
407 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
407 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
408 },
408 },
409
409
410 callbacks: function(){
410 callbacks: function(){
411 /**
411 /**
412 * Create msg callbacks for a comm msg.
412 * Create msg callbacks for a comm msg.
413 */
413 */
414 return this.model.callbacks(this);
414 return this.model.callbacks(this);
415 },
415 },
416
416
417 render: function(){
417 render: function(){
418 /**
418 /**
419 * Render the view.
419 * Render the view.
420 *
420 *
421 * By default, this is only called the first time the view is created
421 * By default, this is only called the first time the view is created
422 */
422 */
423 },
423 },
424
424
425 send: function (content) {
425 send: function (content) {
426 /**
426 /**
427 * Send a custom msg associated with this view.
427 * Send a custom msg associated with this view.
428 */
428 */
429 this.model.send(content, this.callbacks());
429 this.model.send(content, this.callbacks());
430 },
430 },
431
431
432 touch: function () {
432 touch: function () {
433 this.model.save_changes(this.callbacks());
433 this.model.save_changes(this.callbacks());
434 },
434 },
435
435
436 after_displayed: function (callback, context) {
436 after_displayed: function (callback, context) {
437 /**
437 /**
438 * Calls the callback right away is the view is already displayed
438 * Calls the callback right away is the view is already displayed
439 * otherwise, register the callback to the 'displayed' event.
439 * otherwise, register the callback to the 'displayed' event.
440 */
440 */
441 if (this.is_displayed) {
441 if (this.is_displayed) {
442 callback.apply(context);
442 callback.apply(context);
443 } else {
443 } else {
444 this.on('displayed', callback, context);
444 this.on('displayed', callback, context);
445 }
445 }
446 },
446 },
447
447
448 remove: function () {
448 remove: function () {
449 // Raise a remove event when the view is removed.
449 // Raise a remove event when the view is removed.
450 WidgetView.__super__.remove.apply(this, arguments);
450 WidgetView.__super__.remove.apply(this, arguments);
451 this.trigger('remove');
451 this.trigger('remove');
452 }
452 }
453 });
453 });
454
454
455
455
456 var DOMWidgetView = WidgetView.extend({
456 var DOMWidgetView = WidgetView.extend({
457 initialize: function (parameters) {
457 initialize: function (parameters) {
458 /**
458 /**
459 * Public constructor
459 * Public constructor
460 */
460 */
461 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
461 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
462 this.model.on('change:visible', this.update_visible, this);
462 this.model.on('change:visible', this.update_visible, this);
463 this.model.on('change:_css', this.update_css, this);
463 this.model.on('change:_css', this.update_css, this);
464
464
465 this.model.on('change:_dom_classes', function(model, new_classes) {
465 this.model.on('change:_dom_classes', function(model, new_classes) {
466 var old_classes = model.previous('_dom_classes');
466 var old_classes = model.previous('_dom_classes');
467 this.update_classes(old_classes, new_classes);
467 this.update_classes(old_classes, new_classes);
468 }, this);
468 }, this);
469
469
470 this.model.on('change:color', function (model, value) {
470 this.model.on('change:color', function (model, value) {
471 this.update_attr('color', value); }, this);
471 this.update_attr('color', value); }, this);
472
472
473 this.model.on('change:background_color', function (model, value) {
473 this.model.on('change:background_color', function (model, value) {
474 this.update_attr('background', value); }, this);
474 this.update_attr('background', value); }, this);
475
475
476 this.model.on('change:width', function (model, value) {
476 this.model.on('change:width', function (model, value) {
477 this.update_attr('width', value); }, this);
477 this.update_attr('width', value); }, this);
478
478
479 this.model.on('change:height', function (model, value) {
479 this.model.on('change:height', function (model, value) {
480 this.update_attr('height', value); }, this);
480 this.update_attr('height', value); }, this);
481
481
482 this.model.on('change:border_color', function (model, value) {
482 this.model.on('change:border_color', function (model, value) {
483 this.update_attr('border-color', value); }, this);
483 this.update_attr('border-color', value); }, this);
484
484
485 this.model.on('change:border_width', function (model, value) {
485 this.model.on('change:border_width', function (model, value) {
486 this.update_attr('border-width', value); }, this);
486 this.update_attr('border-width', value); }, this);
487
487
488 this.model.on('change:border_style', function (model, value) {
488 this.model.on('change:border_style', function (model, value) {
489 this.update_attr('border-style', value); }, this);
489 this.update_attr('border-style', value); }, this);
490
490
491 this.model.on('change:font_style', function (model, value) {
491 this.model.on('change:font_style', function (model, value) {
492 this.update_attr('font-style', value); }, this);
492 this.update_attr('font-style', value); }, this);
493
493
494 this.model.on('change:font_weight', function (model, value) {
494 this.model.on('change:font_weight', function (model, value) {
495 this.update_attr('font-weight', value); }, this);
495 this.update_attr('font-weight', value); }, this);
496
496
497 this.model.on('change:font_size', function (model, value) {
497 this.model.on('change:font_size', function (model, value) {
498 this.update_attr('font-size', this._default_px(value)); }, this);
498 this.update_attr('font-size', this._default_px(value)); }, this);
499
499
500 this.model.on('change:font_family', function (model, value) {
500 this.model.on('change:font_family', function (model, value) {
501 this.update_attr('font-family', value); }, this);
501 this.update_attr('font-family', value); }, this);
502
502
503 this.model.on('change:padding', function (model, value) {
503 this.model.on('change:padding', function (model, value) {
504 this.update_attr('padding', value); }, this);
504 this.update_attr('padding', value); }, this);
505
505
506 this.model.on('change:margin', function (model, value) {
506 this.model.on('change:margin', function (model, value) {
507 this.update_attr('margin', this._default_px(value)); }, this);
507 this.update_attr('margin', this._default_px(value)); }, this);
508
508
509 this.model.on('change:border_radius', function (model, value) {
509 this.model.on('change:border_radius', function (model, value) {
510 this.update_attr('border-radius', this._default_px(value)); }, this);
510 this.update_attr('border-radius', this._default_px(value)); }, this);
511
511
512 this.after_displayed(function() {
512 this.after_displayed(function() {
513 this.update_visible(this.model, this.model.get("visible"));
513 this.update_visible(this.model, this.model.get("visible"));
514 this.update_classes([], this.model.get('_dom_classes'));
514 this.update_classes([], this.model.get('_dom_classes'));
515
515
516 this.update_attr('color', this.model.get('color'));
516 this.update_attr('color', this.model.get('color'));
517 this.update_attr('background', this.model.get('background_color'));
517 this.update_attr('background', this.model.get('background_color'));
518 this.update_attr('width', this.model.get('width'));
518 this.update_attr('width', this.model.get('width'));
519 this.update_attr('height', this.model.get('height'));
519 this.update_attr('height', this.model.get('height'));
520 this.update_attr('border-color', this.model.get('border_color'));
520 this.update_attr('border-color', this.model.get('border_color'));
521 this.update_attr('border-width', this.model.get('border_width'));
521 this.update_attr('border-width', this.model.get('border_width'));
522 this.update_attr('border-style', this.model.get('border_style'));
522 this.update_attr('border-style', this.model.get('border_style'));
523 this.update_attr('font-style', this.model.get('font_style'));
523 this.update_attr('font-style', this.model.get('font_style'));
524 this.update_attr('font-weight', this.model.get('font_weight'));
524 this.update_attr('font-weight', this.model.get('font_weight'));
525 this.update_attr('font-size', this.model.get('font_size'));
525 this.update_attr('font-size', this.model.get('font_size'));
526 this.update_attr('font-family', this.model.get('font_family'));
526 this.update_attr('font-family', this.model.get('font_family'));
527 this.update_attr('padding', this.model.get('padding'));
527 this.update_attr('padding', this.model.get('padding'));
528 this.update_attr('margin', this.model.get('margin'));
528 this.update_attr('margin', this.model.get('margin'));
529 this.update_attr('border-radius', this.model.get('border_radius'));
529 this.update_attr('border-radius', this.model.get('border_radius'));
530
530
531 this.update_css(this.model, this.model.get("_css"));
531 this.update_css(this.model, this.model.get("_css"));
532 }, this);
532 }, this);
533 },
533 },
534
534
535 _default_px: function(value) {
535 _default_px: function(value) {
536 /**
536 /**
537 * Makes browser interpret a numerical string as a pixel value.
537 * Makes browser interpret a numerical string as a pixel value.
538 */
538 */
539 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
539 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
540 return value.trim() + 'px';
540 return value.trim() + 'px';
541 }
541 }
542 return value;
542 return value;
543 },
543 },
544
544
545 update_attr: function(name, value) {
545 update_attr: function(name, value) {
546 /**
546 /**
547 * Set a css attr of the widget view.
547 * Set a css attr of the widget view.
548 */
548 */
549 this.$el.css(name, value);
549 this.$el.css(name, value);
550 },
550 },
551
551
552 update_visible: function(model, value) {
552 update_visible: function(model, value) {
553 /**
553 /**
554 * Update visibility
554 * Update visibility
555 */
555 */
556 this.$el.toggle(value);
556 switch(value) {
557 case null: // python None
558 this.$el.show().css('visibility', 'hidden'); break;
559 case false:
560 this.$el.hide(); break;
561 case true:
562 this.$el.show().css('visibility', ''); break;
563 }
557 },
564 },
558
565
559 update_css: function (model, css) {
566 update_css: function (model, css) {
560 /**
567 /**
561 * Update the css styling of this view.
568 * Update the css styling of this view.
562 */
569 */
563 var e = this.$el;
570 var e = this.$el;
564 if (css === undefined) {return;}
571 if (css === undefined) {return;}
565 for (var i = 0; i < css.length; i++) {
572 for (var i = 0; i < css.length; i++) {
566 // Apply the css traits to all elements that match the selector.
573 // Apply the css traits to all elements that match the selector.
567 var selector = css[i][0];
574 var selector = css[i][0];
568 var elements = this._get_selector_element(selector);
575 var elements = this._get_selector_element(selector);
569 if (elements.length > 0) {
576 if (elements.length > 0) {
570 var trait_key = css[i][1];
577 var trait_key = css[i][1];
571 var trait_value = css[i][2];
578 var trait_value = css[i][2];
572 elements.css(trait_key ,trait_value);
579 elements.css(trait_key ,trait_value);
573 }
580 }
574 }
581 }
575 },
582 },
576
583
577 update_classes: function (old_classes, new_classes, $el) {
584 update_classes: function (old_classes, new_classes, $el) {
578 /**
585 /**
579 * Update the DOM classes applied to an element, default to this.$el.
586 * Update the DOM classes applied to an element, default to this.$el.
580 */
587 */
581 if ($el===undefined) {
588 if ($el===undefined) {
582 $el = this.$el;
589 $el = this.$el;
583 }
590 }
584 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
591 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
585 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
592 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
586 },
593 },
587
594
588 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
595 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
589 /**
596 /**
590 * Update the DOM classes applied to the widget based on a single
597 * Update the DOM classes applied to the widget based on a single
591 * trait's value.
598 * trait's value.
592 *
599 *
593 * Given a trait value classes map, this function automatically
600 * Given a trait value classes map, this function automatically
594 * handles applying the appropriate classes to the widget element
601 * handles applying the appropriate classes to the widget element
595 * and removing classes that are no longer valid.
602 * and removing classes that are no longer valid.
596 *
603 *
597 * Parameters
604 * Parameters
598 * ----------
605 * ----------
599 * class_map: dictionary
606 * class_map: dictionary
600 * Dictionary of trait values to class lists.
607 * Dictionary of trait values to class lists.
601 * Example:
608 * Example:
602 * {
609 * {
603 * success: ['alert', 'alert-success'],
610 * success: ['alert', 'alert-success'],
604 * info: ['alert', 'alert-info'],
611 * info: ['alert', 'alert-info'],
605 * warning: ['alert', 'alert-warning'],
612 * warning: ['alert', 'alert-warning'],
606 * danger: ['alert', 'alert-danger']
613 * danger: ['alert', 'alert-danger']
607 * };
614 * };
608 * trait_name: string
615 * trait_name: string
609 * Name of the trait to check the value of.
616 * Name of the trait to check the value of.
610 * previous_trait_value: optional string, default ''
617 * previous_trait_value: optional string, default ''
611 * Last trait value
618 * Last trait value
612 * $el: optional jQuery element handle, defaults to this.$el
619 * $el: optional jQuery element handle, defaults to this.$el
613 * Element that the classes are applied to.
620 * Element that the classes are applied to.
614 */
621 */
615 var key = previous_trait_value;
622 var key = previous_trait_value;
616 if (key === undefined) {
623 if (key === undefined) {
617 key = this.model.previous(trait_name);
624 key = this.model.previous(trait_name);
618 }
625 }
619 var old_classes = class_map[key] ? class_map[key] : [];
626 var old_classes = class_map[key] ? class_map[key] : [];
620 key = this.model.get(trait_name);
627 key = this.model.get(trait_name);
621 var new_classes = class_map[key] ? class_map[key] : [];
628 var new_classes = class_map[key] ? class_map[key] : [];
622
629
623 this.update_classes(old_classes, new_classes, $el || this.$el);
630 this.update_classes(old_classes, new_classes, $el || this.$el);
624 },
631 },
625
632
626 _get_selector_element: function (selector) {
633 _get_selector_element: function (selector) {
627 /**
634 /**
628 * Get the elements via the css selector.
635 * Get the elements via the css selector.
629 */
636 */
630 var elements;
637 var elements;
631 if (!selector) {
638 if (!selector) {
632 elements = this.$el;
639 elements = this.$el;
633 } else {
640 } else {
634 elements = this.$el.find(selector).addBack(selector);
641 elements = this.$el.find(selector).addBack(selector);
635 }
642 }
636 return elements;
643 return elements;
637 },
644 },
638
645
639 typeset: function(element, text){
646 typeset: function(element, text){
640 utils.typeset.apply(null, arguments);
647 utils.typeset.apply(null, arguments);
641 },
648 },
642 });
649 });
643
650
644
651
645 var ViewList = function(create_view, remove_view, context) {
652 var ViewList = function(create_view, remove_view, context) {
646 /**
653 /**
647 * - create_view and remove_view are default functions called when adding or removing views
654 * - create_view and remove_view are default functions called when adding or removing views
648 * - create_view takes a model and returns a view or a promise for a view for that model
655 * - create_view takes a model and returns a view or a promise for a view for that model
649 * - remove_view takes a view and destroys it (including calling `view.remove()`)
656 * - remove_view takes a view and destroys it (including calling `view.remove()`)
650 * - each time the update() function is called with a new list, the create and remove
657 * - each time the update() function is called with a new list, the create and remove
651 * callbacks will be called in an order so that if you append the views created in the
658 * callbacks will be called in an order so that if you append the views created in the
652 * create callback and remove the views in the remove callback, you will duplicate
659 * create callback and remove the views in the remove callback, you will duplicate
653 * the order of the list.
660 * the order of the list.
654 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
661 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
655 * - the context defaults to the created ViewList. If you pass another context, the create and remove
662 * - the context defaults to the created ViewList. If you pass another context, the create and remove
656 * will be called in that context.
663 * will be called in that context.
657 */
664 */
658
665
659 this.initialize.apply(this, arguments);
666 this.initialize.apply(this, arguments);
660 };
667 };
661
668
662 _.extend(ViewList.prototype, {
669 _.extend(ViewList.prototype, {
663 initialize: function(create_view, remove_view, context) {
670 initialize: function(create_view, remove_view, context) {
664 this._handler_context = context || this;
671 this._handler_context = context || this;
665 this._models = [];
672 this._models = [];
666 this.views = []; // list of promises for views
673 this.views = []; // list of promises for views
667 this._create_view = create_view;
674 this._create_view = create_view;
668 this._remove_view = remove_view || function(view) {view.remove();};
675 this._remove_view = remove_view || function(view) {view.remove();};
669 },
676 },
670
677
671 update: function(new_models, create_view, remove_view, context) {
678 update: function(new_models, create_view, remove_view, context) {
672 /**
679 /**
673 * the create_view, remove_view, and context arguments override the defaults
680 * the create_view, remove_view, and context arguments override the defaults
674 * specified when the list is created.
681 * specified when the list is created.
675 * after this function, the .views attribute is a list of promises for views
682 * after this function, the .views attribute is a list of promises for views
676 * if you want to perform some action on the list of views, do something like
683 * if you want to perform some action on the list of views, do something like
677 * `Promise.all(myviewlist.views).then(function(views) {...});`
684 * `Promise.all(myviewlist.views).then(function(views) {...});`
678 */
685 */
679 var remove = remove_view || this._remove_view;
686 var remove = remove_view || this._remove_view;
680 var create = create_view || this._create_view;
687 var create = create_view || this._create_view;
681 var context = context || this._handler_context;
688 var context = context || this._handler_context;
682 var i = 0;
689 var i = 0;
683 // first, skip past the beginning of the lists if they are identical
690 // first, skip past the beginning of the lists if they are identical
684 for (; i < new_models.length; i++) {
691 for (; i < new_models.length; i++) {
685 if (i >= this._models.length || new_models[i] !== this._models[i]) {
692 if (i >= this._models.length || new_models[i] !== this._models[i]) {
686 break;
693 break;
687 }
694 }
688 }
695 }
689
696
690 var first_removed = i;
697 var first_removed = i;
691 // Remove the non-matching items from the old list.
698 // Remove the non-matching items from the old list.
692 var removed = this.views.splice(first_removed, this.views.length-first_removed);
699 var removed = this.views.splice(first_removed, this.views.length-first_removed);
693 for (var j = 0; j < removed.length; j++) {
700 for (var j = 0; j < removed.length; j++) {
694 removed[j].then(function(view) {
701 removed[j].then(function(view) {
695 remove.call(context, view)
702 remove.call(context, view)
696 });
703 });
697 }
704 }
698
705
699 // Add the rest of the new list items.
706 // Add the rest of the new list items.
700 for (; i < new_models.length; i++) {
707 for (; i < new_models.length; i++) {
701 this.views.push(Promise.resolve(create.call(context, new_models[i])));
708 this.views.push(Promise.resolve(create.call(context, new_models[i])));
702 }
709 }
703 // make a copy of the input array
710 // make a copy of the input array
704 this._models = new_models.slice();
711 this._models = new_models.slice();
705 },
712 },
706
713
707 remove: function() {
714 remove: function() {
708 /**
715 /**
709 * removes every view in the list; convenience function for `.update([])`
716 * removes every view in the list; convenience function for `.update([])`
710 * that should be faster
717 * that should be faster
711 * returns a promise that resolves after this removal is done
718 * returns a promise that resolves after this removal is done
712 */
719 */
713 var that = this;
720 var that = this;
714 return Promise.all(this.views).then(function(views) {
721 return Promise.all(this.views).then(function(views) {
715 for (var i = 0; i < that.views.length; i++) {
722 for (var i = 0; i < that.views.length; i++) {
716 that._remove_view.call(that._handler_context, views[i]);
723 that._remove_view.call(that._handler_context, views[i]);
717 }
724 }
718 that.views = [];
725 that.views = [];
719 that._models = [];
726 that._models = [];
720 });
727 });
721 },
728 },
722 });
729 });
723
730
724 var widget = {
731 var widget = {
725 'WidgetModel': WidgetModel,
732 'WidgetModel': WidgetModel,
726 'WidgetView': WidgetView,
733 'WidgetView': WidgetView,
727 'DOMWidgetView': DOMWidgetView,
734 'DOMWidgetView': DOMWidgetView,
728 'ViewList': ViewList,
735 'ViewList': ViewList,
729 };
736 };
730
737
731 // For backwards compatability.
738 // For backwards compatability.
732 $.extend(IPython, widget);
739 $.extend(IPython, widget);
733
740
734 return widget;
741 return widget;
735 });
742 });
@@ -1,489 +1,489 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16 import collections
16 import collections
17
17
18 from IPython.core.getipython import get_ipython
18 from IPython.core.getipython import get_ipython
19 from IPython.kernel.comm import Comm
19 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
20 from IPython.config import LoggingConfigurable
21 from IPython.utils.importstring import import_item
21 from IPython.utils.importstring import import_item
22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 from IPython.utils.py3compat import string_types
24 from IPython.utils.py3compat import string_types
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Classes
27 # Classes
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 class CallbackDispatcher(LoggingConfigurable):
29 class CallbackDispatcher(LoggingConfigurable):
30 """A structure for registering and running callbacks"""
30 """A structure for registering and running callbacks"""
31 callbacks = List()
31 callbacks = List()
32
32
33 def __call__(self, *args, **kwargs):
33 def __call__(self, *args, **kwargs):
34 """Call all of the registered callbacks."""
34 """Call all of the registered callbacks."""
35 value = None
35 value = None
36 for callback in self.callbacks:
36 for callback in self.callbacks:
37 try:
37 try:
38 local_value = callback(*args, **kwargs)
38 local_value = callback(*args, **kwargs)
39 except Exception as e:
39 except Exception as e:
40 ip = get_ipython()
40 ip = get_ipython()
41 if ip is None:
41 if ip is None:
42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
43 else:
43 else:
44 ip.showtraceback()
44 ip.showtraceback()
45 else:
45 else:
46 value = local_value if local_value is not None else value
46 value = local_value if local_value is not None else value
47 return value
47 return value
48
48
49 def register_callback(self, callback, remove=False):
49 def register_callback(self, callback, remove=False):
50 """(Un)Register a callback
50 """(Un)Register a callback
51
51
52 Parameters
52 Parameters
53 ----------
53 ----------
54 callback: method handle
54 callback: method handle
55 Method to be registered or unregistered.
55 Method to be registered or unregistered.
56 remove=False: bool
56 remove=False: bool
57 Whether to unregister the callback."""
57 Whether to unregister the callback."""
58
58
59 # (Un)Register the callback.
59 # (Un)Register the callback.
60 if remove and callback in self.callbacks:
60 if remove and callback in self.callbacks:
61 self.callbacks.remove(callback)
61 self.callbacks.remove(callback)
62 elif not remove and callback not in self.callbacks:
62 elif not remove and callback not in self.callbacks:
63 self.callbacks.append(callback)
63 self.callbacks.append(callback)
64
64
65 def _show_traceback(method):
65 def _show_traceback(method):
66 """decorator for showing tracebacks in IPython"""
66 """decorator for showing tracebacks in IPython"""
67 def m(self, *args, **kwargs):
67 def m(self, *args, **kwargs):
68 try:
68 try:
69 return(method(self, *args, **kwargs))
69 return(method(self, *args, **kwargs))
70 except Exception as e:
70 except Exception as e:
71 ip = get_ipython()
71 ip = get_ipython()
72 if ip is None:
72 if ip is None:
73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
74 else:
74 else:
75 ip.showtraceback()
75 ip.showtraceback()
76 return m
76 return m
77
77
78
78
79 def register(key=None):
79 def register(key=None):
80 """Returns a decorator registering a widget class in the widget registry.
80 """Returns a decorator registering a widget class in the widget registry.
81 If no key is provided, the class name is used as a key. A key is
81 If no key is provided, the class name is used as a key. A key is
82 provided for each core IPython widget so that the frontend can use
82 provided for each core IPython widget so that the frontend can use
83 this key regardless of the language of the kernel"""
83 this key regardless of the language of the kernel"""
84 def wrap(widget):
84 def wrap(widget):
85 l = key if key is not None else widget.__module__ + widget.__name__
85 l = key if key is not None else widget.__module__ + widget.__name__
86 Widget.widget_types[l] = widget
86 Widget.widget_types[l] = widget
87 return widget
87 return widget
88 return wrap
88 return wrap
89
89
90
90
91 class Widget(LoggingConfigurable):
91 class Widget(LoggingConfigurable):
92 #-------------------------------------------------------------------------
92 #-------------------------------------------------------------------------
93 # Class attributes
93 # Class attributes
94 #-------------------------------------------------------------------------
94 #-------------------------------------------------------------------------
95 _widget_construction_callback = None
95 _widget_construction_callback = None
96 widgets = {}
96 widgets = {}
97 widget_types = {}
97 widget_types = {}
98
98
99 @staticmethod
99 @staticmethod
100 def on_widget_constructed(callback):
100 def on_widget_constructed(callback):
101 """Registers a callback to be called when a widget is constructed.
101 """Registers a callback to be called when a widget is constructed.
102
102
103 The callback must have the following signature:
103 The callback must have the following signature:
104 callback(widget)"""
104 callback(widget)"""
105 Widget._widget_construction_callback = callback
105 Widget._widget_construction_callback = callback
106
106
107 @staticmethod
107 @staticmethod
108 def _call_widget_constructed(widget):
108 def _call_widget_constructed(widget):
109 """Static method, called when a widget is constructed."""
109 """Static method, called when a widget is constructed."""
110 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
110 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
111 Widget._widget_construction_callback(widget)
111 Widget._widget_construction_callback(widget)
112
112
113 @staticmethod
113 @staticmethod
114 def handle_comm_opened(comm, msg):
114 def handle_comm_opened(comm, msg):
115 """Static method, called when a widget is constructed."""
115 """Static method, called when a widget is constructed."""
116 widget_class = import_item(msg['content']['data']['widget_class'])
116 widget_class = import_item(msg['content']['data']['widget_class'])
117 widget = widget_class(comm=comm)
117 widget = widget_class(comm=comm)
118
118
119
119
120 #-------------------------------------------------------------------------
120 #-------------------------------------------------------------------------
121 # Traits
121 # Traits
122 #-------------------------------------------------------------------------
122 #-------------------------------------------------------------------------
123 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
123 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
124 in which to find _model_name. If empty, look in the global registry.""")
124 in which to find _model_name. If empty, look in the global registry.""")
125 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
125 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
126 registered in the front-end to create and sync this widget with.""")
126 registered in the front-end to create and sync this widget with.""")
127 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
127 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
128 If empty, look in the global registry.""", sync=True)
128 If empty, look in the global registry.""", sync=True)
129 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
129 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
130 to use to represent the widget.""", sync=True)
130 to use to represent the widget.""", sync=True)
131 comm = Instance('IPython.kernel.comm.Comm')
131 comm = Instance('IPython.kernel.comm.Comm')
132
132
133 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
133 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
134 front-end can send before receiving an idle msg from the back-end.""")
134 front-end can send before receiving an idle msg from the back-end.""")
135
135
136 version = Int(0, sync=True, help="""Widget's version""")
136 version = Int(0, sync=True, help="""Widget's version""")
137 keys = List()
137 keys = List()
138 def _keys_default(self):
138 def _keys_default(self):
139 return [name for name in self.traits(sync=True)]
139 return [name for name in self.traits(sync=True)]
140
140
141 _property_lock = Tuple((None, None))
141 _property_lock = Tuple((None, None))
142 _send_state_lock = Int(0)
142 _send_state_lock = Int(0)
143 _states_to_send = Set(allow_none=False)
143 _states_to_send = Set(allow_none=False)
144 _display_callbacks = Instance(CallbackDispatcher, ())
144 _display_callbacks = Instance(CallbackDispatcher, ())
145 _msg_callbacks = Instance(CallbackDispatcher, ())
145 _msg_callbacks = Instance(CallbackDispatcher, ())
146
146
147 #-------------------------------------------------------------------------
147 #-------------------------------------------------------------------------
148 # (Con/de)structor
148 # (Con/de)structor
149 #-------------------------------------------------------------------------
149 #-------------------------------------------------------------------------
150 def __init__(self, **kwargs):
150 def __init__(self, **kwargs):
151 """Public constructor"""
151 """Public constructor"""
152 self._model_id = kwargs.pop('model_id', None)
152 self._model_id = kwargs.pop('model_id', None)
153 super(Widget, self).__init__(**kwargs)
153 super(Widget, self).__init__(**kwargs)
154
154
155 Widget._call_widget_constructed(self)
155 Widget._call_widget_constructed(self)
156 self.open()
156 self.open()
157
157
158 def __del__(self):
158 def __del__(self):
159 """Object disposal"""
159 """Object disposal"""
160 self.close()
160 self.close()
161
161
162 #-------------------------------------------------------------------------
162 #-------------------------------------------------------------------------
163 # Properties
163 # Properties
164 #-------------------------------------------------------------------------
164 #-------------------------------------------------------------------------
165
165
166 def open(self):
166 def open(self):
167 """Open a comm to the frontend if one isn't already open."""
167 """Open a comm to the frontend if one isn't already open."""
168 if self.comm is None:
168 if self.comm is None:
169 args = dict(target_name='ipython.widget',
169 args = dict(target_name='ipython.widget',
170 data={'model_name': self._model_name,
170 data={'model_name': self._model_name,
171 'model_module': self._model_module})
171 'model_module': self._model_module})
172 if self._model_id is not None:
172 if self._model_id is not None:
173 args['comm_id'] = self._model_id
173 args['comm_id'] = self._model_id
174 self.comm = Comm(**args)
174 self.comm = Comm(**args)
175
175
176 def _comm_changed(self, name, new):
176 def _comm_changed(self, name, new):
177 """Called when the comm is changed."""
177 """Called when the comm is changed."""
178 if new is None:
178 if new is None:
179 return
179 return
180 self._model_id = self.model_id
180 self._model_id = self.model_id
181
181
182 self.comm.on_msg(self._handle_msg)
182 self.comm.on_msg(self._handle_msg)
183 Widget.widgets[self.model_id] = self
183 Widget.widgets[self.model_id] = self
184
184
185 # first update
185 # first update
186 self.send_state()
186 self.send_state()
187
187
188 @property
188 @property
189 def model_id(self):
189 def model_id(self):
190 """Gets the model id of this widget.
190 """Gets the model id of this widget.
191
191
192 If a Comm doesn't exist yet, a Comm will be created automagically."""
192 If a Comm doesn't exist yet, a Comm will be created automagically."""
193 return self.comm.comm_id
193 return self.comm.comm_id
194
194
195 #-------------------------------------------------------------------------
195 #-------------------------------------------------------------------------
196 # Methods
196 # Methods
197 #-------------------------------------------------------------------------
197 #-------------------------------------------------------------------------
198
198
199 def close(self):
199 def close(self):
200 """Close method.
200 """Close method.
201
201
202 Closes the underlying comm.
202 Closes the underlying comm.
203 When the comm is closed, all of the widget views are automatically
203 When the comm is closed, all of the widget views are automatically
204 removed from the front-end."""
204 removed from the front-end."""
205 if self.comm is not None:
205 if self.comm is not None:
206 Widget.widgets.pop(self.model_id, None)
206 Widget.widgets.pop(self.model_id, None)
207 self.comm.close()
207 self.comm.close()
208 self.comm = None
208 self.comm = None
209
209
210 def send_state(self, key=None):
210 def send_state(self, key=None):
211 """Sends the widget state, or a piece of it, to the front-end.
211 """Sends the widget state, or a piece of it, to the front-end.
212
212
213 Parameters
213 Parameters
214 ----------
214 ----------
215 key : unicode, or iterable (optional)
215 key : unicode, or iterable (optional)
216 A single property's name or iterable of property names to sync with the front-end.
216 A single property's name or iterable of property names to sync with the front-end.
217 """
217 """
218 self._send({
218 self._send({
219 "method" : "update",
219 "method" : "update",
220 "state" : self.get_state(key=key)
220 "state" : self.get_state(key=key)
221 })
221 })
222
222
223 def get_state(self, key=None):
223 def get_state(self, key=None):
224 """Gets the widget state, or a piece of it.
224 """Gets the widget state, or a piece of it.
225
225
226 Parameters
226 Parameters
227 ----------
227 ----------
228 key : unicode or iterable (optional)
228 key : unicode or iterable (optional)
229 A single property's name or iterable of property names to get.
229 A single property's name or iterable of property names to get.
230 """
230 """
231 if key is None:
231 if key is None:
232 keys = self.keys
232 keys = self.keys
233 elif isinstance(key, string_types):
233 elif isinstance(key, string_types):
234 keys = [key]
234 keys = [key]
235 elif isinstance(key, collections.Iterable):
235 elif isinstance(key, collections.Iterable):
236 keys = key
236 keys = key
237 else:
237 else:
238 raise ValueError("key must be a string, an iterable of keys, or None")
238 raise ValueError("key must be a string, an iterable of keys, or None")
239 state = {}
239 state = {}
240 for k in keys:
240 for k in keys:
241 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
241 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
242 value = getattr(self, k)
242 value = getattr(self, k)
243 state[k] = f(value)
243 state[k] = f(value)
244 return state
244 return state
245
245
246 def set_state(self, sync_data):
246 def set_state(self, sync_data):
247 """Called when a state is received from the front-end."""
247 """Called when a state is received from the front-end."""
248 for name in self.keys:
248 for name in self.keys:
249 if name in sync_data:
249 if name in sync_data:
250 json_value = sync_data[name]
250 json_value = sync_data[name]
251 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
251 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
252 with self._lock_property(name, json_value):
252 with self._lock_property(name, json_value):
253 setattr(self, name, from_json(json_value))
253 setattr(self, name, from_json(json_value))
254
254
255 def send(self, content):
255 def send(self, content):
256 """Sends a custom msg to the widget model in the front-end.
256 """Sends a custom msg to the widget model in the front-end.
257
257
258 Parameters
258 Parameters
259 ----------
259 ----------
260 content : dict
260 content : dict
261 Content of the message to send.
261 Content of the message to send.
262 """
262 """
263 self._send({"method": "custom", "content": content})
263 self._send({"method": "custom", "content": content})
264
264
265 def on_msg(self, callback, remove=False):
265 def on_msg(self, callback, remove=False):
266 """(Un)Register a custom msg receive callback.
266 """(Un)Register a custom msg receive callback.
267
267
268 Parameters
268 Parameters
269 ----------
269 ----------
270 callback: callable
270 callback: callable
271 callback will be passed two arguments when a message arrives::
271 callback will be passed two arguments when a message arrives::
272
272
273 callback(widget, content)
273 callback(widget, content)
274
274
275 remove: bool
275 remove: bool
276 True if the callback should be unregistered."""
276 True if the callback should be unregistered."""
277 self._msg_callbacks.register_callback(callback, remove=remove)
277 self._msg_callbacks.register_callback(callback, remove=remove)
278
278
279 def on_displayed(self, callback, remove=False):
279 def on_displayed(self, callback, remove=False):
280 """(Un)Register a widget displayed callback.
280 """(Un)Register a widget displayed callback.
281
281
282 Parameters
282 Parameters
283 ----------
283 ----------
284 callback: method handler
284 callback: method handler
285 Must have a signature of::
285 Must have a signature of::
286
286
287 callback(widget, **kwargs)
287 callback(widget, **kwargs)
288
288
289 kwargs from display are passed through without modification.
289 kwargs from display are passed through without modification.
290 remove: bool
290 remove: bool
291 True if the callback should be unregistered."""
291 True if the callback should be unregistered."""
292 self._display_callbacks.register_callback(callback, remove=remove)
292 self._display_callbacks.register_callback(callback, remove=remove)
293
293
294 #-------------------------------------------------------------------------
294 #-------------------------------------------------------------------------
295 # Support methods
295 # Support methods
296 #-------------------------------------------------------------------------
296 #-------------------------------------------------------------------------
297 @contextmanager
297 @contextmanager
298 def _lock_property(self, key, value):
298 def _lock_property(self, key, value):
299 """Lock a property-value pair.
299 """Lock a property-value pair.
300
300
301 The value should be the JSON state of the property.
301 The value should be the JSON state of the property.
302
302
303 NOTE: This, in addition to the single lock for all state changes, is
303 NOTE: This, in addition to the single lock for all state changes, is
304 flawed. In the future we may want to look into buffering state changes
304 flawed. In the future we may want to look into buffering state changes
305 back to the front-end."""
305 back to the front-end."""
306 self._property_lock = (key, value)
306 self._property_lock = (key, value)
307 try:
307 try:
308 yield
308 yield
309 finally:
309 finally:
310 self._property_lock = (None, None)
310 self._property_lock = (None, None)
311
311
312 @contextmanager
312 @contextmanager
313 def hold_sync(self):
313 def hold_sync(self):
314 """Hold syncing any state until the context manager is released"""
314 """Hold syncing any state until the context manager is released"""
315 # We increment a value so that this can be nested. Syncing will happen when
315 # We increment a value so that this can be nested. Syncing will happen when
316 # all levels have been released.
316 # all levels have been released.
317 self._send_state_lock += 1
317 self._send_state_lock += 1
318 try:
318 try:
319 yield
319 yield
320 finally:
320 finally:
321 self._send_state_lock -=1
321 self._send_state_lock -=1
322 if self._send_state_lock == 0:
322 if self._send_state_lock == 0:
323 self.send_state(self._states_to_send)
323 self.send_state(self._states_to_send)
324 self._states_to_send.clear()
324 self._states_to_send.clear()
325
325
326 def _should_send_property(self, key, value):
326 def _should_send_property(self, key, value):
327 """Check the property lock (property_lock)"""
327 """Check the property lock (property_lock)"""
328 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
328 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
329 if (key == self._property_lock[0]
329 if (key == self._property_lock[0]
330 and to_json(value) == self._property_lock[1]):
330 and to_json(value) == self._property_lock[1]):
331 return False
331 return False
332 elif self._send_state_lock > 0:
332 elif self._send_state_lock > 0:
333 self._states_to_send.add(key)
333 self._states_to_send.add(key)
334 return False
334 return False
335 else:
335 else:
336 return True
336 return True
337
337
338 # Event handlers
338 # Event handlers
339 @_show_traceback
339 @_show_traceback
340 def _handle_msg(self, msg):
340 def _handle_msg(self, msg):
341 """Called when a msg is received from the front-end"""
341 """Called when a msg is received from the front-end"""
342 data = msg['content']['data']
342 data = msg['content']['data']
343 method = data['method']
343 method = data['method']
344
344
345 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
345 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
346 if method == 'backbone':
346 if method == 'backbone':
347 if 'sync_data' in data:
347 if 'sync_data' in data:
348 sync_data = data['sync_data']
348 sync_data = data['sync_data']
349 self.set_state(sync_data) # handles all methods
349 self.set_state(sync_data) # handles all methods
350
350
351 # Handle a state request.
351 # Handle a state request.
352 elif method == 'request_state':
352 elif method == 'request_state':
353 self.send_state()
353 self.send_state()
354
354
355 # Handle a custom msg from the front-end.
355 # Handle a custom msg from the front-end.
356 elif method == 'custom':
356 elif method == 'custom':
357 if 'content' in data:
357 if 'content' in data:
358 self._handle_custom_msg(data['content'])
358 self._handle_custom_msg(data['content'])
359
359
360 # Catch remainder.
360 # Catch remainder.
361 else:
361 else:
362 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
362 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
363
363
364 def _handle_custom_msg(self, content):
364 def _handle_custom_msg(self, content):
365 """Called when a custom msg is received."""
365 """Called when a custom msg is received."""
366 self._msg_callbacks(self, content)
366 self._msg_callbacks(self, content)
367
367
368 def _notify_trait(self, name, old_value, new_value):
368 def _notify_trait(self, name, old_value, new_value):
369 """Called when a property has been changed."""
369 """Called when a property has been changed."""
370 # Trigger default traitlet callback machinery. This allows any user
370 # Trigger default traitlet callback machinery. This allows any user
371 # registered validation to be processed prior to allowing the widget
371 # registered validation to be processed prior to allowing the widget
372 # machinery to handle the state.
372 # machinery to handle the state.
373 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
373 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
374
374
375 # Send the state after the user registered callbacks for trait changes
375 # Send the state after the user registered callbacks for trait changes
376 # have all fired (allows for user to validate values).
376 # have all fired (allows for user to validate values).
377 if self.comm is not None and name in self.keys:
377 if self.comm is not None and name in self.keys:
378 # Make sure this isn't information that the front-end just sent us.
378 # Make sure this isn't information that the front-end just sent us.
379 if self._should_send_property(name, new_value):
379 if self._should_send_property(name, new_value):
380 # Send new state to front-end
380 # Send new state to front-end
381 self.send_state(key=name)
381 self.send_state(key=name)
382
382
383 def _handle_displayed(self, **kwargs):
383 def _handle_displayed(self, **kwargs):
384 """Called when a view has been displayed for this widget instance"""
384 """Called when a view has been displayed for this widget instance"""
385 self._display_callbacks(self, **kwargs)
385 self._display_callbacks(self, **kwargs)
386
386
387 def _trait_to_json(self, x):
387 def _trait_to_json(self, x):
388 """Convert a trait value to json
388 """Convert a trait value to json
389
389
390 Traverse lists/tuples and dicts and serialize their values as well.
390 Traverse lists/tuples and dicts and serialize their values as well.
391 Replace any widgets with their model_id
391 Replace any widgets with their model_id
392 """
392 """
393 if isinstance(x, dict):
393 if isinstance(x, dict):
394 return {k: self._trait_to_json(v) for k, v in x.items()}
394 return {k: self._trait_to_json(v) for k, v in x.items()}
395 elif isinstance(x, (list, tuple)):
395 elif isinstance(x, (list, tuple)):
396 return [self._trait_to_json(v) for v in x]
396 return [self._trait_to_json(v) for v in x]
397 elif isinstance(x, Widget):
397 elif isinstance(x, Widget):
398 return "IPY_MODEL_" + x.model_id
398 return "IPY_MODEL_" + x.model_id
399 else:
399 else:
400 return x # Value must be JSON-able
400 return x # Value must be JSON-able
401
401
402 def _trait_from_json(self, x):
402 def _trait_from_json(self, x):
403 """Convert json values to objects
403 """Convert json values to objects
404
404
405 Replace any strings representing valid model id values to Widget references.
405 Replace any strings representing valid model id values to Widget references.
406 """
406 """
407 if isinstance(x, dict):
407 if isinstance(x, dict):
408 return {k: self._trait_from_json(v) for k, v in x.items()}
408 return {k: self._trait_from_json(v) for k, v in x.items()}
409 elif isinstance(x, (list, tuple)):
409 elif isinstance(x, (list, tuple)):
410 return [self._trait_from_json(v) for v in x]
410 return [self._trait_from_json(v) for v in x]
411 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
411 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
412 # we want to support having child widgets at any level in a hierarchy
412 # we want to support having child widgets at any level in a hierarchy
413 # trusting that a widget UUID will not appear out in the wild
413 # trusting that a widget UUID will not appear out in the wild
414 return Widget.widgets[x[10:]]
414 return Widget.widgets[x[10:]]
415 else:
415 else:
416 return x
416 return x
417
417
418 def _ipython_display_(self, **kwargs):
418 def _ipython_display_(self, **kwargs):
419 """Called when `IPython.display.display` is called on the widget."""
419 """Called when `IPython.display.display` is called on the widget."""
420 # Show view.
420 # Show view.
421 if self._view_name is not None:
421 if self._view_name is not None:
422 self._send({"method": "display"})
422 self._send({"method": "display"})
423 self._handle_displayed(**kwargs)
423 self._handle_displayed(**kwargs)
424
424
425 def _send(self, msg):
425 def _send(self, msg):
426 """Sends a message to the model in the front-end."""
426 """Sends a message to the model in the front-end."""
427 self.comm.send(msg)
427 self.comm.send(msg)
428
428
429
429
430 class DOMWidget(Widget):
430 class DOMWidget(Widget):
431 visible = Bool(True, help="Whether the widget is visible.", sync=True)
431 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)
432 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
432 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
433 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
433 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
434
434
435 width = CUnicode(sync=True)
435 width = CUnicode(sync=True)
436 height = CUnicode(sync=True)
436 height = CUnicode(sync=True)
437 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
437 # A default padding of 2.5 px makes the widgets look nice when displayed inline.
438 padding = CUnicode("2.5px", sync=True)
438 padding = CUnicode("2.5px", sync=True)
439 margin = CUnicode(sync=True)
439 margin = CUnicode(sync=True)
440
440
441 color = Unicode(sync=True)
441 color = Unicode(sync=True)
442 background_color = Unicode(sync=True)
442 background_color = Unicode(sync=True)
443 border_color = Unicode(sync=True)
443 border_color = Unicode(sync=True)
444
444
445 border_width = CUnicode(sync=True)
445 border_width = CUnicode(sync=True)
446 border_radius = CUnicode(sync=True)
446 border_radius = CUnicode(sync=True)
447 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
447 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
448 'none',
448 'none',
449 'hidden',
449 'hidden',
450 'dotted',
450 'dotted',
451 'dashed',
451 'dashed',
452 'solid',
452 'solid',
453 'double',
453 'double',
454 'groove',
454 'groove',
455 'ridge',
455 'ridge',
456 'inset',
456 'inset',
457 'outset',
457 'outset',
458 'initial',
458 'initial',
459 'inherit', ''],
459 'inherit', ''],
460 default_value='', sync=True)
460 default_value='', sync=True)
461
461
462 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
462 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
463 'normal',
463 'normal',
464 'italic',
464 'italic',
465 'oblique',
465 'oblique',
466 'initial',
466 'initial',
467 'inherit', ''],
467 'inherit', ''],
468 default_value='', sync=True)
468 default_value='', sync=True)
469 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
469 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
470 'normal',
470 'normal',
471 'bold',
471 'bold',
472 'bolder',
472 'bolder',
473 'lighter',
473 'lighter',
474 'initial',
474 'initial',
475 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
475 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
476 default_value='', sync=True)
476 default_value='', sync=True)
477 font_size = CUnicode(sync=True)
477 font_size = CUnicode(sync=True)
478 font_family = Unicode(sync=True)
478 font_family = Unicode(sync=True)
479
479
480 def __init__(self, *pargs, **kwargs):
480 def __init__(self, *pargs, **kwargs):
481 super(DOMWidget, self).__init__(*pargs, **kwargs)
481 super(DOMWidget, self).__init__(*pargs, **kwargs)
482
482
483 def _validate_border(name, old, new):
483 def _validate_border(name, old, new):
484 if new is not None and new != '':
484 if new is not None and new != '':
485 if name != 'border_width' and not self.border_width:
485 if name != 'border_width' and not self.border_width:
486 self.border_width = 1
486 self.border_width = 1
487 if name != 'border_style' and self.border_style == '':
487 if name != 'border_style' and self.border_style == '':
488 self.border_style = 'solid'
488 self.border_style = 'solid'
489 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
489 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,752 +1,775 b''
1 {
1 {
2 "cells": [
2 "cells": [
3 {
3 {
4 "cell_type": "markdown",
4 "cell_type": "markdown",
5 "metadata": {},
5 "metadata": {},
6 "source": [
6 "source": [
7 "[Index](Index.ipynb) - [Back](Widget Events.ipynb) - [Next](Custom Widget - Hello World.ipynb)"
7 "[Index](Index.ipynb) - [Back](Widget Events.ipynb) - [Next](Custom Widget - Hello World.ipynb)"
8 ]
8 ]
9 },
9 },
10 {
10 {
11 "cell_type": "code",
11 "cell_type": "code",
12 "execution_count": null,
12 "execution_count": null,
13 "metadata": {
13 "metadata": {
14 "collapsed": false
14 "collapsed": false
15 },
15 },
16 "outputs": [],
16 "outputs": [],
17 "source": [
17 "source": [
18 "%%html\n",
18 "%%html\n",
19 "<style>\n",
19 "<style>\n",
20 ".example-container { background: #999999; padding: 2px; min-height: 100px; }\n",
20 ".example-container { background: #999999; padding: 2px; min-height: 100px; }\n",
21 ".example-container.sm { min-height: 50px; }\n",
21 ".example-container.sm { min-height: 50px; }\n",
22 ".example-box { background: #9999FF; width: 50px; height: 50px; text-align: center; vertical-align: middle; color: white; font-weight: bold; margin: 2px;}\n",
22 ".example-box { background: #9999FF; width: 50px; height: 50px; text-align: center; vertical-align: middle; color: white; font-weight: bold; margin: 2px;}\n",
23 ".example-box.med { width: 65px; height: 65px; } \n",
23 ".example-box.med { width: 65px; height: 65px; } \n",
24 ".example-box.lrg { width: 80px; height: 80px; } \n",
24 ".example-box.lrg { width: 80px; height: 80px; } \n",
25 "</style>"
25 "</style>"
26 ]
26 ]
27 },
27 },
28 {
28 {
29 "cell_type": "code",
29 "cell_type": "code",
30 "execution_count": null,
30 "execution_count": null,
31 "metadata": {
31 "metadata": {
32 "collapsed": false
32 "collapsed": false
33 },
33 },
34 "outputs": [],
34 "outputs": [],
35 "source": [
35 "source": [
36 "from IPython.html import widgets\n",
36 "from IPython.html import widgets\n",
37 "from IPython.display import display"
37 "from IPython.display import display"
38 ]
38 ]
39 },
39 },
40 {
40 {
41 "cell_type": "markdown",
41 "cell_type": "markdown",
42 "metadata": {
42 "metadata": {
43 "slideshow": {
43 "slideshow": {
44 "slide_type": "slide"
44 "slide_type": "slide"
45 }
45 }
46 },
46 },
47 "source": [
47 "source": [
48 "# Widget Styling"
48 "# Widget Styling"
49 ]
49 ]
50 },
50 },
51 {
51 {
52 "cell_type": "markdown",
52 "cell_type": "markdown",
53 "metadata": {},
53 "metadata": {},
54 "source": [
54 "source": [
55 "## Basic styling"
55 "## Basic styling"
56 ]
56 ]
57 },
57 },
58 {
58 {
59 "cell_type": "markdown",
59 "cell_type": "markdown",
60 "metadata": {},
60 "metadata": {},
61 "source": [
61 "source": [
62 "The widgets distributed with IPython can be styled by setting the following traits:\n",
62 "The widgets distributed with IPython can be styled by setting the following traits:\n",
63 "\n",
63 "\n",
64 "- width \n",
64 "- width \n",
65 "- height \n",
65 "- height \n",
66 "- fore_color \n",
66 "- fore_color \n",
67 "- back_color \n",
67 "- back_color \n",
68 "- border_color \n",
68 "- border_color \n",
69 "- border_width \n",
69 "- border_width \n",
70 "- border_style \n",
70 "- border_style \n",
71 "- font_style \n",
71 "- font_style \n",
72 "- font_weight \n",
72 "- font_weight \n",
73 "- font_size \n",
73 "- font_size \n",
74 "- font_family \n",
74 "- font_family \n",
75 "\n",
75 "\n",
76 "The example below shows how a `Button` widget can be styled:"
76 "The example below shows how a `Button` widget can be styled:"
77 ]
77 ]
78 },
78 },
79 {
79 {
80 "cell_type": "code",
80 "cell_type": "code",
81 "execution_count": null,
81 "execution_count": null,
82 "metadata": {
82 "metadata": {
83 "collapsed": false
83 "collapsed": false
84 },
84 },
85 "outputs": [],
85 "outputs": [],
86 "source": [
86 "source": [
87 "button = widgets.Button(\n",
87 "button = widgets.Button(\n",
88 " description='Hello World!',\n",
88 " description='Hello World!',\n",
89 " width=100, # Integers are interpreted as pixel measurements.\n",
89 " width=100, # Integers are interpreted as pixel measurements.\n",
90 " height='2em', # em is valid HTML unit of measurement.\n",
90 " height='2em', # em is valid HTML unit of measurement.\n",
91 " color='lime', # Colors can be set by name,\n",
91 " color='lime', # Colors can be set by name,\n",
92 " background_color='#0022FF', # and also by color code.\n",
92 " background_color='#0022FF', # and also by color code.\n",
93 " border_color='red')\n",
93 " border_color='red')\n",
94 "display(button)"
94 "display(button)"
95 ]
95 ]
96 },
96 },
97 {
97 {
98 "cell_type": "markdown",
98 "cell_type": "markdown",
99 "metadata": {
99 "metadata": {
100 "slideshow": {
100 "slideshow": {
101 "slide_type": "slide"
101 "slide_type": "slide"
102 }
102 }
103 },
103 },
104 "source": [
104 "source": [
105 "## Parent/child relationships"
105 "## Parent/child relationships"
106 ]
106 ]
107 },
107 },
108 {
108 {
109 "cell_type": "markdown",
109 "cell_type": "markdown",
110 "metadata": {},
110 "metadata": {},
111 "source": [
111 "source": [
112 "To display widget A inside widget B, widget A must be a child of widget B. Widgets that can contain other widgets have a **`children` attribute**. This attribute can be **set via a keyword argument** in the widget's constructor **or after construction**. Calling display on an **object with children automatically displays those children**, too."
112 "To display widget A inside widget B, widget A must be a child of widget B. Widgets that can contain other widgets have a **`children` attribute**. This attribute can be **set via a keyword argument** in the widget's constructor **or after construction**. Calling display on an **object with children automatically displays those children**, too."
113 ]
113 ]
114 },
114 },
115 {
115 {
116 "cell_type": "code",
116 "cell_type": "code",
117 "execution_count": null,
117 "execution_count": null,
118 "metadata": {
118 "metadata": {
119 "collapsed": false
119 "collapsed": false
120 },
120 },
121 "outputs": [],
121 "outputs": [],
122 "source": [
122 "source": [
123 "from IPython.display import display\n",
123 "from IPython.display import display\n",
124 "\n",
124 "\n",
125 "float_range = widgets.FloatSlider()\n",
125 "float_range = widgets.FloatSlider()\n",
126 "string = widgets.Text(value='hi')\n",
126 "string = widgets.Text(value='hi')\n",
127 "container = widgets.Box(children=[float_range, string])\n",
127 "container = widgets.Box(children=[float_range, string])\n",
128 "\n",
128 "\n",
129 "container.border_color = 'red'\n",
129 "container.border_color = 'red'\n",
130 "container.border_style = 'dotted'\n",
130 "container.border_style = 'dotted'\n",
131 "container.border_width = 3\n",
131 "container.border_width = 3\n",
132 "display(container) # Displays the `container` and all of it's children."
132 "display(container) # Displays the `container` and all of it's children."
133 ]
133 ]
134 },
134 },
135 {
135 {
136 "cell_type": "markdown",
136 "cell_type": "markdown",
137 "metadata": {},
137 "metadata": {},
138 "source": [
138 "source": [
139 "### After the parent is displayed"
139 "### After the parent is displayed"
140 ]
140 ]
141 },
141 },
142 {
142 {
143 "cell_type": "markdown",
143 "cell_type": "markdown",
144 "metadata": {
144 "metadata": {
145 "slideshow": {
145 "slideshow": {
146 "slide_type": "slide"
146 "slide_type": "slide"
147 }
147 }
148 },
148 },
149 "source": [
149 "source": [
150 "Children **can be added to parents** after the parent has been displayed. The **parent is responsible for rendering its children**."
150 "Children **can be added to parents** after the parent has been displayed. The **parent is responsible for rendering its children**."
151 ]
151 ]
152 },
152 },
153 {
153 {
154 "cell_type": "code",
154 "cell_type": "code",
155 "execution_count": null,
155 "execution_count": null,
156 "metadata": {
156 "metadata": {
157 "collapsed": false
157 "collapsed": false
158 },
158 },
159 "outputs": [],
159 "outputs": [],
160 "source": [
160 "source": [
161 "container = widgets.Box()\n",
161 "container = widgets.Box()\n",
162 "container.border_color = 'red'\n",
162 "container.border_color = 'red'\n",
163 "container.border_style = 'dotted'\n",
163 "container.border_style = 'dotted'\n",
164 "container.border_width = 3\n",
164 "container.border_width = 3\n",
165 "display(container)\n",
165 "display(container)\n",
166 "\n",
166 "\n",
167 "int_range = widgets.IntSlider()\n",
167 "int_range = widgets.IntSlider()\n",
168 "container.children=[int_range]"
168 "container.children=[int_range]"
169 ]
169 ]
170 },
170 },
171 {
171 {
172 "cell_type": "markdown",
172 "cell_type": "markdown",
173 "metadata": {
173 "metadata": {
174 "slideshow": {
174 "slideshow": {
175 "slide_type": "slide"
175 "slide_type": "slide"
176 }
176 }
177 },
177 },
178 "source": [
178 "source": [
179 "## Fancy boxes"
179 "## Fancy boxes"
180 ]
180 ]
181 },
181 },
182 {
182 {
183 "cell_type": "markdown",
183 "cell_type": "markdown",
184 "metadata": {},
184 "metadata": {},
185 "source": [
185 "source": [
186 "If you need to display a more complicated set of widgets, there are **specialized containers** that you can use. To display **multiple sets of widgets**, you can use an **`Accordion` or a `Tab` in combination with one `Box` per set of widgets** (as seen below). The \"pages\" of these widgets are their children. To set the titles of the pages, one must **call `set_title` after the widget has been displayed**."
186 "If you need to display a more complicated set of widgets, there are **specialized containers** that you can use. To display **multiple sets of widgets**, you can use an **`Accordion` or a `Tab` in combination with one `Box` per set of widgets** (as seen below). The \"pages\" of these widgets are their children. To set the titles of the pages, one must **call `set_title` after the widget has been displayed**."
187 ]
187 ]
188 },
188 },
189 {
189 {
190 "cell_type": "markdown",
190 "cell_type": "markdown",
191 "metadata": {},
191 "metadata": {},
192 "source": [
192 "source": [
193 "### Accordion"
193 "### Accordion"
194 ]
194 ]
195 },
195 },
196 {
196 {
197 "cell_type": "code",
197 "cell_type": "code",
198 "execution_count": null,
198 "execution_count": null,
199 "metadata": {
199 "metadata": {
200 "collapsed": false
200 "collapsed": false
201 },
201 },
202 "outputs": [],
202 "outputs": [],
203 "source": [
203 "source": [
204 "name1 = widgets.Text(description='Location:')\n",
204 "name1 = widgets.Text(description='Location:')\n",
205 "zip1 = widgets.BoundedIntText(description='Zip:', min=0, max=99999)\n",
205 "zip1 = widgets.BoundedIntText(description='Zip:', min=0, max=99999)\n",
206 "page1 = widgets.Box(children=[name1, zip1])\n",
206 "page1 = widgets.Box(children=[name1, zip1])\n",
207 "\n",
207 "\n",
208 "name2 = widgets.Text(description='Location:')\n",
208 "name2 = widgets.Text(description='Location:')\n",
209 "zip2 = widgets.BoundedIntText(description='Zip:', min=0, max=99999)\n",
209 "zip2 = widgets.BoundedIntText(description='Zip:', min=0, max=99999)\n",
210 "page2 = widgets.Box(children=[name2, zip2])\n",
210 "page2 = widgets.Box(children=[name2, zip2])\n",
211 "\n",
211 "\n",
212 "accord = widgets.Accordion(children=[page1, page2])\n",
212 "accord = widgets.Accordion(children=[page1, page2])\n",
213 "display(accord)\n",
213 "display(accord)\n",
214 "\n",
214 "\n",
215 "accord.set_title(0, 'From')\n",
215 "accord.set_title(0, 'From')\n",
216 "accord.set_title(1, 'To')"
216 "accord.set_title(1, 'To')"
217 ]
217 ]
218 },
218 },
219 {
219 {
220 "cell_type": "markdown",
220 "cell_type": "markdown",
221 "metadata": {
221 "metadata": {
222 "slideshow": {
222 "slideshow": {
223 "slide_type": "slide"
223 "slide_type": "slide"
224 }
224 }
225 },
225 },
226 "source": [
226 "source": [
227 "### TabWidget"
227 "### TabWidget"
228 ]
228 ]
229 },
229 },
230 {
230 {
231 "cell_type": "code",
231 "cell_type": "code",
232 "execution_count": null,
232 "execution_count": null,
233 "metadata": {
233 "metadata": {
234 "collapsed": false
234 "collapsed": false
235 },
235 },
236 "outputs": [],
236 "outputs": [],
237 "source": [
237 "source": [
238 "name = widgets.Text(description='Name:')\n",
238 "name = widgets.Text(description='Name:')\n",
239 "color = widgets.Dropdown(description='Color:', values=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])\n",
239 "color = widgets.Dropdown(description='Color:', values=['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'])\n",
240 "page1 = widgets.Box(children=[name, color])\n",
240 "page1 = widgets.Box(children=[name, color])\n",
241 "\n",
241 "\n",
242 "age = widgets.IntSlider(description='Age:', min=0, max=120, value=50)\n",
242 "age = widgets.IntSlider(description='Age:', min=0, max=120, value=50)\n",
243 "gender = widgets.RadioButtons(description='Gender:', values=['male', 'female'])\n",
243 "gender = widgets.RadioButtons(description='Gender:', values=['male', 'female'])\n",
244 "page2 = widgets.Box(children=[age, gender])\n",
244 "page2 = widgets.Box(children=[age, gender])\n",
245 "\n",
245 "\n",
246 "tabs = widgets.Tab(children=[page1, page2])\n",
246 "tabs = widgets.Tab(children=[page1, page2])\n",
247 "display(tabs)\n",
247 "display(tabs)\n",
248 "\n",
248 "\n",
249 "tabs.set_title(0, 'Name')\n",
249 "tabs.set_title(0, 'Name')\n",
250 "tabs.set_title(1, 'Details')"
250 "tabs.set_title(1, 'Details')"
251 ]
251 ]
252 },
252 },
253 {
253 {
254 "cell_type": "markdown",
254 "cell_type": "markdown",
255 "metadata": {
255 "metadata": {
256 "slideshow": {
256 "slideshow": {
257 "slide_type": "slide"
257 "slide_type": "slide"
258 }
258 }
259 },
259 },
260 "source": [
260 "source": [
261 "### Popup"
261 "### Popup"
262 ]
262 ]
263 },
263 },
264 {
264 {
265 "cell_type": "markdown",
265 "cell_type": "markdown",
266 "metadata": {},
266 "metadata": {},
267 "source": [
267 "source": [
268 "Unlike the other two special containers, the `Popup` is only **designed to display one set of widgets**. The `Popup` can be used to **display widgets outside of the widget area**. "
268 "Unlike the other two special containers, the `Popup` is only **designed to display one set of widgets**. The `Popup` can be used to **display widgets outside of the widget area**. "
269 ]
269 ]
270 },
270 },
271 {
271 {
272 "cell_type": "code",
272 "cell_type": "code",
273 "execution_count": null,
273 "execution_count": null,
274 "metadata": {
274 "metadata": {
275 "collapsed": false
275 "collapsed": false
276 },
276 },
277 "outputs": [],
277 "outputs": [],
278 "source": [
278 "source": [
279 "counter = widgets.IntText(description='Counter:')\n",
279 "counter = widgets.IntText(description='Counter:')\n",
280 "popup = widgets.Popup(children=[counter], description='Popup Demo', button_text='Popup Button')\n",
280 "popup = widgets.Popup(children=[counter], description='Popup Demo', button_text='Popup Button')\n",
281 "display(popup)"
281 "display(popup)"
282 ]
282 ]
283 },
283 },
284 {
284 {
285 "cell_type": "code",
285 "cell_type": "code",
286 "execution_count": null,
286 "execution_count": null,
287 "metadata": {
287 "metadata": {
288 "collapsed": false
288 "collapsed": false
289 },
289 },
290 "outputs": [],
290 "outputs": [],
291 "source": [
291 "source": [
292 "counter.value += 1"
292 "counter.value += 1"
293 ]
293 ]
294 },
294 },
295 {
295 {
296 "cell_type": "code",
296 "cell_type": "code",
297 "execution_count": null,
297 "execution_count": null,
298 "metadata": {
298 "metadata": {
299 "collapsed": false
299 "collapsed": false
300 },
300 },
301 "outputs": [],
301 "outputs": [],
302 "source": []
302 "source": []
303 },
303 },
304 {
304 {
305 "cell_type": "code",
305 "cell_type": "code",
306 "execution_count": null,
306 "execution_count": null,
307 "metadata": {
307 "metadata": {
308 "collapsed": false
308 "collapsed": false
309 },
309 },
310 "outputs": [],
310 "outputs": [],
311 "source": []
311 "source": []
312 },
312 },
313 {
313 {
314 "cell_type": "code",
314 "cell_type": "code",
315 "execution_count": null,
315 "execution_count": null,
316 "metadata": {
316 "metadata": {
317 "collapsed": false
317 "collapsed": false
318 },
318 },
319 "outputs": [],
319 "outputs": [],
320 "source": []
320 "source": []
321 },
321 },
322 {
322 {
323 "cell_type": "code",
323 "cell_type": "code",
324 "execution_count": null,
324 "execution_count": null,
325 "metadata": {
325 "metadata": {
326 "collapsed": false
326 "collapsed": false
327 },
327 },
328 "outputs": [],
328 "outputs": [],
329 "source": []
329 "source": []
330 },
330 },
331 {
331 {
332 "cell_type": "code",
332 "cell_type": "code",
333 "execution_count": null,
333 "execution_count": null,
334 "metadata": {
334 "metadata": {
335 "collapsed": false
335 "collapsed": false
336 },
336 },
337 "outputs": [],
337 "outputs": [],
338 "source": []
338 "source": []
339 },
339 },
340 {
340 {
341 "cell_type": "code",
341 "cell_type": "code",
342 "execution_count": null,
342 "execution_count": null,
343 "metadata": {
343 "metadata": {
344 "collapsed": false
344 "collapsed": false
345 },
345 },
346 "outputs": [],
346 "outputs": [],
347 "source": []
347 "source": []
348 },
348 },
349 {
349 {
350 "cell_type": "code",
350 "cell_type": "code",
351 "execution_count": null,
351 "execution_count": null,
352 "metadata": {
352 "metadata": {
353 "collapsed": false
353 "collapsed": false
354 },
354 },
355 "outputs": [],
355 "outputs": [],
356 "source": []
356 "source": []
357 },
357 },
358 {
358 {
359 "cell_type": "code",
359 "cell_type": "code",
360 "execution_count": null,
360 "execution_count": null,
361 "metadata": {
361 "metadata": {
362 "collapsed": false
362 "collapsed": false
363 },
363 },
364 "outputs": [],
364 "outputs": [],
365 "source": []
365 "source": []
366 },
366 },
367 {
367 {
368 "cell_type": "code",
368 "cell_type": "code",
369 "execution_count": null,
369 "execution_count": null,
370 "metadata": {
370 "metadata": {
371 "collapsed": false
371 "collapsed": false
372 },
372 },
373 "outputs": [],
373 "outputs": [],
374 "source": []
374 "source": []
375 },
375 },
376 {
376 {
377 "cell_type": "code",
377 "cell_type": "code",
378 "execution_count": null,
378 "execution_count": null,
379 "metadata": {
379 "metadata": {
380 "collapsed": false
380 "collapsed": false
381 },
381 },
382 "outputs": [],
382 "outputs": [],
383 "source": []
383 "source": []
384 },
384 },
385 {
385 {
386 "cell_type": "code",
386 "cell_type": "code",
387 "execution_count": null,
387 "execution_count": null,
388 "metadata": {
388 "metadata": {
389 "collapsed": false
389 "collapsed": false
390 },
390 },
391 "outputs": [],
391 "outputs": [],
392 "source": []
392 "source": []
393 },
393 },
394 {
394 {
395 "cell_type": "code",
395 "cell_type": "code",
396 "execution_count": null,
396 "execution_count": null,
397 "metadata": {
397 "metadata": {
398 "collapsed": false
398 "collapsed": false
399 },
399 },
400 "outputs": [],
400 "outputs": [],
401 "source": []
401 "source": []
402 },
402 },
403 {
403 {
404 "cell_type": "code",
404 "cell_type": "code",
405 "execution_count": null,
405 "execution_count": null,
406 "metadata": {
406 "metadata": {
407 "collapsed": false
407 "collapsed": false
408 },
408 },
409 "outputs": [],
409 "outputs": [],
410 "source": []
410 "source": []
411 },
411 },
412 {
412 {
413 "cell_type": "code",
413 "cell_type": "code",
414 "execution_count": null,
414 "execution_count": null,
415 "metadata": {
415 "metadata": {
416 "collapsed": false
416 "collapsed": false
417 },
417 },
418 "outputs": [],
418 "outputs": [],
419 "source": []
419 "source": []
420 },
420 },
421 {
421 {
422 "cell_type": "code",
422 "cell_type": "code",
423 "execution_count": null,
423 "execution_count": null,
424 "metadata": {
424 "metadata": {
425 "collapsed": false
425 "collapsed": false
426 },
426 },
427 "outputs": [],
427 "outputs": [],
428 "source": [
428 "source": [
429 "counter.value += 1"
429 "counter.value += 1"
430 ]
430 ]
431 },
431 },
432 {
432 {
433 "cell_type": "code",
433 "cell_type": "code",
434 "execution_count": null,
434 "execution_count": null,
435 "metadata": {
435 "metadata": {
436 "collapsed": false
436 "collapsed": false
437 },
437 },
438 "outputs": [],
438 "outputs": [],
439 "source": [
439 "source": [
440 "popup.close()"
440 "popup.close()"
441 ]
441 ]
442 },
442 },
443 {
443 {
444 "cell_type": "markdown",
444 "cell_type": "markdown",
445 "metadata": {
445 "metadata": {
446 "slideshow": {
446 "slideshow": {
447 "slide_type": "slide"
447 "slide_type": "slide"
448 }
448 }
449 },
449 },
450 "source": [
450 "source": [
451 "# Alignment"
451 "# Alignment"
452 ]
452 ]
453 },
453 },
454 {
454 {
455 "cell_type": "markdown",
455 "cell_type": "markdown",
456 "metadata": {},
456 "metadata": {},
457 "source": [
457 "source": [
458 "Most widgets have a **`description` attribute**, which allows a label for the widget to be defined.\n",
458 "Most widgets have a **`description` attribute**, which allows a label for the widget to be defined.\n",
459 "The label of the widget **has a fixed minimum width**.\n",
459 "The label of the widget **has a fixed minimum width**.\n",
460 "The text of the label is **always right aligned and the widget is left aligned**:"
460 "The text of the label is **always right aligned and the widget is left aligned**:"
461 ]
461 ]
462 },
462 },
463 {
463 {
464 "cell_type": "code",
464 "cell_type": "code",
465 "execution_count": null,
465 "execution_count": null,
466 "metadata": {
466 "metadata": {
467 "collapsed": false
467 "collapsed": false
468 },
468 },
469 "outputs": [],
469 "outputs": [],
470 "source": [
470 "source": [
471 "display(widgets.Text(description=\"a:\"))\n",
471 "display(widgets.Text(description=\"a:\"))\n",
472 "display(widgets.Text(description=\"aa:\"))\n",
472 "display(widgets.Text(description=\"aa:\"))\n",
473 "display(widgets.Text(description=\"aaa:\"))"
473 "display(widgets.Text(description=\"aaa:\"))"
474 ]
474 ]
475 },
475 },
476 {
476 {
477 "cell_type": "markdown",
477 "cell_type": "markdown",
478 "metadata": {
478 "metadata": {
479 "slideshow": {
479 "slideshow": {
480 "slide_type": "slide"
480 "slide_type": "slide"
481 }
481 }
482 },
482 },
483 "source": [
483 "source": [
484 "If a **label is longer** than the minimum width, the **widget is shifted to the right**:"
484 "If a **label is longer** than the minimum width, the **widget is shifted to the right**:"
485 ]
485 ]
486 },
486 },
487 {
487 {
488 "cell_type": "code",
488 "cell_type": "code",
489 "execution_count": null,
489 "execution_count": null,
490 "metadata": {
490 "metadata": {
491 "collapsed": false
491 "collapsed": false
492 },
492 },
493 "outputs": [],
493 "outputs": [],
494 "source": [
494 "source": [
495 "display(widgets.Text(description=\"a:\"))\n",
495 "display(widgets.Text(description=\"a:\"))\n",
496 "display(widgets.Text(description=\"aa:\"))\n",
496 "display(widgets.Text(description=\"aa:\"))\n",
497 "display(widgets.Text(description=\"aaa:\"))\n",
497 "display(widgets.Text(description=\"aaa:\"))\n",
498 "display(widgets.Text(description=\"aaaaaaaaaaaaaaaaaa:\"))"
498 "display(widgets.Text(description=\"aaaaaaaaaaaaaaaaaa:\"))"
499 ]
499 ]
500 },
500 },
501 {
501 {
502 "cell_type": "markdown",
502 "cell_type": "markdown",
503 "metadata": {
503 "metadata": {
504 "slideshow": {
504 "slideshow": {
505 "slide_type": "slide"
505 "slide_type": "slide"
506 }
506 }
507 },
507 },
508 "source": [
508 "source": [
509 "If a `description` is **not set** for the widget, the **label is not displayed**:"
509 "If a `description` is **not set** for the widget, the **label is not displayed**:"
510 ]
510 ]
511 },
511 },
512 {
512 {
513 "cell_type": "code",
513 "cell_type": "code",
514 "execution_count": null,
514 "execution_count": null,
515 "metadata": {
515 "metadata": {
516 "collapsed": false
516 "collapsed": false
517 },
517 },
518 "outputs": [],
518 "outputs": [],
519 "source": [
519 "source": [
520 "display(widgets.Text(description=\"a:\"))\n",
520 "display(widgets.Text(description=\"a:\"))\n",
521 "display(widgets.Text(description=\"aa:\"))\n",
521 "display(widgets.Text(description=\"aa:\"))\n",
522 "display(widgets.Text(description=\"aaa:\"))\n",
522 "display(widgets.Text(description=\"aaa:\"))\n",
523 "display(widgets.Text())"
523 "display(widgets.Text())"
524 ]
524 ]
525 },
525 },
526 {
526 {
527 "cell_type": "markdown",
527 "cell_type": "markdown",
528 "metadata": {
528 "metadata": {
529 "slideshow": {
529 "slideshow": {
530 "slide_type": "slide"
530 "slide_type": "slide"
531 }
531 }
532 },
532 },
533 "source": [
533 "source": [
534 "## Flex boxes"
534 "## Flex boxes"
535 ]
535 ]
536 },
536 },
537 {
537 {
538 "cell_type": "markdown",
538 "cell_type": "markdown",
539 "metadata": {},
539 "metadata": {},
540 "source": [
540 "source": [
541 "Widgets can be aligned using the `FlexBox`, `HBox`, and `VBox` widgets."
541 "Widgets can be aligned using the `FlexBox`, `HBox`, and `VBox` widgets."
542 ]
542 ]
543 },
543 },
544 {
544 {
545 "cell_type": "markdown",
545 "cell_type": "markdown",
546 "metadata": {
546 "metadata": {
547 "slideshow": {
547 "slideshow": {
548 "slide_type": "slide"
548 "slide_type": "slide"
549 }
549 }
550 },
550 },
551 "source": [
551 "source": [
552 "### Application to widgets"
552 "### Application to widgets"
553 ]
553 ]
554 },
554 },
555 {
555 {
556 "cell_type": "markdown",
556 "cell_type": "markdown",
557 "metadata": {},
557 "metadata": {},
558 "source": [
558 "source": [
559 "Widgets display vertically by default:"
559 "Widgets display vertically by default:"
560 ]
560 ]
561 },
561 },
562 {
562 {
563 "cell_type": "code",
563 "cell_type": "code",
564 "execution_count": null,
564 "execution_count": null,
565 "metadata": {
565 "metadata": {
566 "collapsed": false
566 "collapsed": false
567 },
567 },
568 "outputs": [],
568 "outputs": [],
569 "source": [
569 "source": [
570 "buttons = [widgets.Button(description=str(i)) for i in range(3)]\n",
570 "buttons = [widgets.Button(description=str(i)) for i in range(3)]\n",
571 "display(*buttons)"
571 "display(*buttons)"
572 ]
572 ]
573 },
573 },
574 {
574 {
575 "cell_type": "markdown",
575 "cell_type": "markdown",
576 "metadata": {
576 "metadata": {
577 "slideshow": {
577 "slideshow": {
578 "slide_type": "slide"
578 "slide_type": "slide"
579 }
579 }
580 },
580 },
581 "source": [
581 "source": [
582 "### Using hbox"
582 "### Using hbox"
583 ]
583 ]
584 },
584 },
585 {
585 {
586 "cell_type": "markdown",
586 "cell_type": "markdown",
587 "metadata": {},
587 "metadata": {},
588 "source": [
588 "source": [
589 "To make widgets display horizontally, you need to **child them to a `HBox` widget**."
589 "To make widgets display horizontally, you need to **child them to a `HBox` widget**."
590 ]
590 ]
591 },
591 },
592 {
592 {
593 "cell_type": "code",
593 "cell_type": "code",
594 "execution_count": null,
594 "execution_count": null,
595 "metadata": {
595 "metadata": {
596 "collapsed": false
596 "collapsed": false
597 },
597 },
598 "outputs": [],
598 "outputs": [],
599 "source": [
599 "source": [
600 "container = widgets.HBox(children=buttons)\n",
600 "container = widgets.HBox(children=buttons)\n",
601 "display(container)"
601 "display(container)"
602 ]
602 ]
603 },
603 },
604 {
604 {
605 "cell_type": "markdown",
605 "cell_type": "markdown",
606 "metadata": {},
606 "metadata": {},
607 "source": [
607 "source": [
608 "By setting the width of the container to 100% and its `pack` to `center`, you can center the buttons."
608 "By setting the width of the container to 100% and its `pack` to `center`, you can center the buttons."
609 ]
609 ]
610 },
610 },
611 {
611 {
612 "cell_type": "code",
612 "cell_type": "code",
613 "execution_count": null,
613 "execution_count": null,
614 "metadata": {
614 "metadata": {
615 "collapsed": false
615 "collapsed": false
616 },
616 },
617 "outputs": [],
617 "outputs": [],
618 "source": [
618 "source": [
619 "container.width = '100%'\n",
619 "container.width = '100%'\n",
620 "container.pack = 'center'"
620 "container.pack = 'center'"
621 ]
621 ]
622 },
622 },
623 {
623 {
624 "cell_type": "markdown",
624 "cell_type": "markdown",
625 "metadata": {
625 "metadata": {
626 "slideshow": {
626 "slideshow": {
627 "slide_type": "slide"
627 "slide_type": "slide"
628 }
628 }
629 },
629 },
630 "source": [
630 "source": [
631 "## Visibility"
631 "## Visibility"
632 ]
632 ]
633 },
633 },
634 {
634 {
635 "cell_type": "markdown",
635 "cell_type": "markdown",
636 "metadata": {},
636 "metadata": {},
637 "source": [
637 "source": [
638 "Sometimes it is necessary to **hide or show widgets** in place, **without having to re-display** the widget.\n",
638 "Sometimes it is necessary to **hide or show widgets** in place, **without having to re-display** the widget.\n",
639 "The `visibility` property of widgets can be used to hide or show **widgets that have already been displayed** (as seen below)."
639 "The `visible` property of widgets can be used to hide or show **widgets that have already been displayed** (as seen below). The `visible` property can be:\n",
640 "* `True` - the widget is displayed\n",
641 "* `False` - the widget is hidden, and the empty space where the widget would be is collapsed\n",
642 "* `None` - the widget is hidden, and the empty space where the widget would be is shown"
640 ]
643 ]
641 },
644 },
642 {
645 {
643 "cell_type": "code",
646 "cell_type": "code",
644 "execution_count": null,
647 "execution_count": null,
645 "metadata": {
648 "metadata": {
646 "collapsed": false
649 "collapsed": false
647 },
650 },
648 "outputs": [],
651 "outputs": [],
649 "source": [
652 "source": [
650 "string = widgets.Latex(value=\"Hello World!\")\n",
653 "w1 = widgets.Latex(value=\"First line\")\n",
651 "display(string) "
654 "w2 = widgets.Latex(value=\"Second line\")\n",
655 "w3 = widgets.Latex(value=\"Third line\")\n",
656 "display(w1, w2, w3)"
657 ]
658 },
659 {
660 "cell_type": "code",
661 "execution_count": null,
662 "metadata": {
663 "collapsed": true
664 },
665 "outputs": [],
666 "source": [
667 "w2.visible=None"
652 ]
668 ]
653 },
669 },
654 {
670 {
655 "cell_type": "code",
671 "cell_type": "code",
656 "execution_count": null,
672 "execution_count": null,
657 "metadata": {
673 "metadata": {
658 "collapsed": false
674 "collapsed": false
659 },
675 },
660 "outputs": [],
676 "outputs": [],
661 "source": [
677 "source": [
662 "string.visible=False"
678 "w2.visible=False"
663 ]
679 ]
664 },
680 },
665 {
681 {
666 "cell_type": "code",
682 "cell_type": "code",
667 "execution_count": null,
683 "execution_count": null,
668 "metadata": {
684 "metadata": {
669 "collapsed": false
685 "collapsed": false
670 },
686 },
671 "outputs": [],
687 "outputs": [],
672 "source": [
688 "source": [
673 "string.visible=True"
689 "w2.visible=True"
674 ]
690 ]
675 },
691 },
676 {
692 {
677 "cell_type": "markdown",
693 "cell_type": "markdown",
678 "metadata": {
694 "metadata": {
679 "slideshow": {
695 "slideshow": {
680 "slide_type": "slide"
696 "slide_type": "slide"
681 }
697 }
682 },
698 },
683 "source": [
699 "source": [
684 "### Another example"
700 "### Another example"
685 ]
701 ]
686 },
702 },
687 {
703 {
688 "cell_type": "markdown",
704 "cell_type": "markdown",
689 "metadata": {},
705 "metadata": {},
690 "source": [
706 "source": [
691 "In the example below, a form is rendered, which conditionally displays widgets depending on the state of other widgets. Try toggling the student checkbox."
707 "In the example below, a form is rendered, which conditionally displays widgets depending on the state of other widgets. Try toggling the student checkbox."
692 ]
708 ]
693 },
709 },
694 {
710 {
695 "cell_type": "code",
711 "cell_type": "code",
696 "execution_count": null,
712 "execution_count": null,
697 "metadata": {
713 "metadata": {
698 "collapsed": false
714 "collapsed": false
699 },
715 },
700 "outputs": [],
716 "outputs": [],
701 "source": [
717 "source": [
702 "form = widgets.VBox()\n",
718 "form = widgets.VBox()\n",
703 "first = widgets.Text(description=\"First Name:\")\n",
719 "first = widgets.Text(description=\"First Name:\")\n",
704 "last = widgets.Text(description=\"Last Name:\")\n",
720 "last = widgets.Text(description=\"Last Name:\")\n",
705 "\n",
721 "\n",
706 "student = widgets.Checkbox(description=\"Student:\", value=False)\n",
722 "student = widgets.Checkbox(description=\"Student:\", value=False)\n",
707 "school_info = widgets.VBox(visible=False, children=[\n",
723 "school_info = widgets.VBox(visible=False, children=[\n",
708 " widgets.Text(description=\"School:\"),\n",
724 " widgets.Text(description=\"School:\"),\n",
709 " widgets.IntText(description=\"Grade:\", min=0, max=12)\n",
725 " widgets.IntText(description=\"Grade:\", min=0, max=12)\n",
710 " ])\n",
726 " ])\n",
711 "\n",
727 "\n",
712 "pet = widgets.Text(description=\"Pet's Name:\")\n",
728 "pet = widgets.Text(description=\"Pet's Name:\")\n",
713 "form.children = [first, last, student, school_info, pet]\n",
729 "form.children = [first, last, student, school_info, pet]\n",
714 "display(form)\n",
730 "display(form)\n",
715 "\n",
731 "\n",
716 "def on_student_toggle(name, value):\n",
732 "def on_student_toggle(name, value):\n",
717 " if value:\n",
733 " if value:\n",
718 " school_info.visible = True\n",
734 " school_info.visible = True\n",
719 " else:\n",
735 " else:\n",
720 " school_info.visible = False\n",
736 " school_info.visible = False\n",
721 "student.on_trait_change(on_student_toggle, 'value')\n"
737 "student.on_trait_change(on_student_toggle, 'value')\n"
722 ]
738 ]
723 },
739 },
724 {
740 {
725 "cell_type": "markdown",
741 "cell_type": "markdown",
726 "metadata": {},
742 "metadata": {},
727 "source": [
743 "source": [
728 "[Index](Index.ipynb) - [Back](Widget Events.ipynb) - [Next](Custom Widget - Hello World.ipynb)"
744 "[Index](Index.ipynb) - [Back](Widget Events.ipynb) - [Next](Custom Widget - Hello World.ipynb)"
729 ]
745 ]
730 }
746 }
731 ],
747 ],
732 "metadata": {
748 "metadata": {
733 "cell_tags": [
749 "cell_tags": [
734 [
750 [
735 "<None>",
751 "<None>",
736 null
752 null
737 ]
753 ]
738 ],
754 ],
739 "kernelspec": {
755 "kernelspec": {
756 "display_name": "Python 2",
757 "name": "python2"
758 },
759 "language_info": {
740 "codemirror_mode": {
760 "codemirror_mode": {
741 "name": "python",
761 "name": "ipython",
742 "version": 2
762 "version": 2
743 },
763 },
744 "display_name": "Python 2",
764 "file_extension": ".py",
745 "language": "python",
765 "mimetype": "text/x-python",
746 "name": "python2"
766 "name": "python",
767 "nbconvert_exporter": "python",
768 "pygments_lexer": "ipython2",
769 "version": "2.7.8"
747 },
770 },
748 "signature": "sha256:8bb091be85fd5e7f76082b1b4b98cddec92a856334935ac2c702fe5ec03f6eff"
771 "signature": "sha256:198630bf2c2eb00401b60a395ebc75049099864b62f0faaf416da02f9808c40b"
749 },
772 },
750 "nbformat": 4,
773 "nbformat": 4,
751 "nbformat_minor": 0
774 "nbformat_minor": 0
752 } No newline at end of file
775 }
General Comments 0
You need to be logged in to leave comments. Login now