##// END OF EJS Templates
Fix some bugs found by the widget examples,...
Jonathan Frederic -
Show More
@@ -1,504 +1,537 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define(["widgets/js/manager",
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace",
9 9 ], function(widgetmanager, _, Backbone, $, IPython){
10 10
11 11 var WidgetModel = Backbone.Model.extend({
12 12 constructor: function (widget_manager, model_id, comm) {
13 13 // Constructor
14 14 //
15 15 // Creates a WidgetModel instance.
16 16 //
17 17 // Parameters
18 18 // ----------
19 19 // widget_manager : WidgetManager instance
20 20 // model_id : string
21 21 // An ID unique to this model.
22 22 // comm : Comm instance (optional)
23 23 this.widget_manager = widget_manager;
24 24 this._buffered_state_diff = {};
25 25 this.pending_msgs = 0;
26 26 this.msg_buffer = null;
27 27 this.key_value_lock = null;
28 28 this.id = model_id;
29 29 this.views = [];
30 30
31 31 if (comm !== undefined) {
32 32 // Remember comm associated with the model.
33 33 this.comm = comm;
34 34 comm.model = this;
35 35
36 36 // Hook comm messages up to model.
37 37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 39 }
40 40 return Backbone.Model.apply(this);
41 41 },
42 42
43 43 send: function (content, callbacks) {
44 44 // Send a custom msg over the comm.
45 45 if (this.comm !== undefined) {
46 46 var data = {method: 'custom', content: content};
47 47 this.comm.send(data, callbacks);
48 48 this.pending_msgs++;
49 49 }
50 50 },
51 51
52 52 _handle_comm_closed: function (msg) {
53 53 // Handle when a widget is closed.
54 54 this.trigger('comm:close');
55 55 delete this.comm.model; // Delete ref so GC will collect widget model.
56 56 delete this.comm;
57 57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 58 _.each(this.views, function(view, i) {
59 59 view.remove();
60 60 });
61 61 },
62 62
63 63 _handle_comm_msg: function (msg) {
64 64 // Handle incoming comm msg.
65 65 var method = msg.content.data.method;
66 66 switch (method) {
67 67 case 'update':
68 68 this.apply_update(msg.content.data.state);
69 69 break;
70 70 case 'custom':
71 71 this.trigger('msg:custom', msg.content.data.content);
72 72 break;
73 73 case 'display':
74 74 this.widget_manager.display_view(msg, this);
75 75 break;
76 76 }
77 77 },
78 78
79 79 apply_update: function (state) {
80 80 // Handle when a widget is updated via the python side.
81 81 var that = this;
82 82 _.each(state, function(value, key) {
83 83 that.key_value_lock = [key, value];
84 84 try {
85 85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
86 86 } finally {
87 87 that.key_value_lock = null;
88 88 }
89 89 });
90 90 },
91 91
92 92 _handle_status: function (msg, callbacks) {
93 93 // Handle status msgs.
94 94
95 95 // execution_state : ('busy', 'idle', 'starting')
96 96 if (this.comm !== undefined) {
97 97 if (msg.content.execution_state ==='idle') {
98 98 // Send buffer if this message caused another message to be
99 99 // throttled.
100 100 if (this.msg_buffer !== null &&
101 101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 103 this.comm.send(data, callbacks);
104 104 this.msg_buffer = null;
105 105 } else {
106 106 --this.pending_msgs;
107 107 }
108 108 }
109 109 }
110 110 },
111 111
112 112 callbacks: function(view) {
113 113 // Create msg callbacks for a comm msg.
114 114 var callbacks = this.widget_manager.callbacks(view);
115 115
116 116 if (callbacks.iopub === undefined) {
117 117 callbacks.iopub = {};
118 118 }
119 119
120 120 var that = this;
121 121 callbacks.iopub.status = function (msg) {
122 122 that._handle_status(msg, callbacks);
123 123 };
124 124 return callbacks;
125 125 },
126 126
127 127 set: function(key, val, options) {
128 128 // Set a value.
129 129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130 130
131 131 // Backbone only remembers the diff of the most recent set()
132 132 // operation. Calling set multiple times in a row results in a
133 133 // loss of diff information. Here we keep our own running diff.
134 134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 135 return return_value;
136 136 },
137 137
138 138 sync: function (method, model, options) {
139 139 // Handle sync to the back-end. Called when a model.save() is called.
140 140
141 141 // Make sure a comm exists.
142 142 var error = options.error || function() {
143 143 console.error('Backbone sync error:', arguments);
144 144 };
145 145 if (this.comm === undefined) {
146 146 error();
147 147 return false;
148 148 }
149 149
150 150 // Delete any key value pairs that the back-end already knows about.
151 151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 152 if (this.key_value_lock !== null) {
153 153 var key = this.key_value_lock[0];
154 154 var value = this.key_value_lock[1];
155 155 if (attrs[key] === value) {
156 156 delete attrs[key];
157 157 }
158 158 }
159 159
160 160 // Only sync if there are attributes to send to the back-end.
161 161 attrs = this._pack_models(attrs);
162 162 if (_.size(attrs) > 0) {
163 163
164 164 // If this message was sent via backbone itself, it will not
165 165 // have any callbacks. It's important that we create callbacks
166 166 // so we can listen for status messages, etc...
167 167 var callbacks = options.callbacks || this.callbacks();
168 168
169 169 // Check throttle.
170 170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 171 // The throttle has been exceeded, buffer the current msg so
172 172 // it can be sent once the kernel has finished processing
173 173 // some of the existing messages.
174 174
175 175 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 176 switch (method) {
177 177 case 'patch':
178 178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 179 break;
180 180 case 'update':
181 181 case 'create':
182 182 this.msg_buffer = attrs;
183 183 break;
184 184 default:
185 185 error();
186 186 return false;
187 187 }
188 188 this.msg_buffer_callbacks = callbacks;
189 189
190 190 } else {
191 191 // We haven't exceeded the throttle, send the message like
192 192 // normal.
193 193 var data = {method: 'backbone', sync_data: attrs};
194 194 this.comm.send(data, callbacks);
195 195 this.pending_msgs++;
196 196 }
197 197 }
198 198 // Since the comm is a one-way communication, assume the message
199 199 // arrived. Don't call success since we don't have a model back from the server
200 200 // this means we miss out on the 'sync' event.
201 201 this._buffered_state_diff = {};
202 202 },
203 203
204 204 save_changes: function(callbacks) {
205 205 // Push this model's state to the back-end
206 206 //
207 207 // This invokes a Backbone.Sync.
208 208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 209 },
210 210
211 211 _pack_models: function(value) {
212 212 // Replace models with model ids recursively.
213 213 var that = this;
214 214 var packed;
215 215 if (value instanceof Backbone.Model) {
216 216 return "IPY_MODEL_" + value.id;
217 217
218 218 } else if ($.isArray(value)) {
219 219 packed = [];
220 220 _.each(value, function(sub_value, key) {
221 221 packed.push(that._pack_models(sub_value));
222 222 });
223 223 return packed;
224 224
225 225 } else if (value instanceof Object) {
226 226 packed = {};
227 227 _.each(value, function(sub_value, key) {
228 228 packed[key] = that._pack_models(sub_value);
229 229 });
230 230 return packed;
231 231
232 232 } else {
233 233 return value;
234 234 }
235 235 },
236 236
237 237 _unpack_models: function(value) {
238 238 // Replace model ids with models recursively.
239 239 var that = this;
240 240 var unpacked;
241 241 if ($.isArray(value)) {
242 242 unpacked = [];
243 243 _.each(value, function(sub_value, key) {
244 244 unpacked.push(that._unpack_models(sub_value));
245 245 });
246 246 return unpacked;
247 247
248 248 } else if (value instanceof Object) {
249 249 unpacked = {};
250 250 _.each(value, function(sub_value, key) {
251 251 unpacked[key] = that._unpack_models(sub_value);
252 252 });
253 253 return unpacked;
254 254
255 255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 257 if (model) {
258 258 return model;
259 259 } else {
260 260 return value;
261 261 }
262 262 } else {
263 263 return value;
264 264 }
265 265 },
266 266
267 267 });
268 268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
269 269
270 270
271 271 var WidgetView = Backbone.View.extend({
272 272 initialize: function(parameters) {
273 273 // Public constructor.
274 274 this.model.on('change',this.update,this);
275 275 this.options = parameters.options;
276 276 this.child_model_views = {};
277 277 this.child_views = {};
278 278 this.model.views.push(this);
279 279 this.id = this.id || IPython.utils.uuid();
280 280 this.on('displayed', function() {
281 281 this.is_displayed = true;
282 282 }, this);
283 283 },
284 284
285 285 update: function(){
286 286 // Triggered on model change.
287 287 //
288 288 // Update view to be consistent with this.model
289 289 },
290 290
291 291 create_child_view: function(child_model, options) {
292 292 // Create and return a child view.
293 293 //
294 294 // -given a model and (optionally) a view name if the view name is
295 295 // not given, it defaults to the model's default view attribute.
296 296
297 297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
298 298 // it would be great to have the widget manager add the cell metadata
299 299 // to the subview without having to add it here.
300 300 options = $.extend({ parent: this }, options || {});
301 301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
302 302
303 303 // Associate the view id with the model id.
304 304 if (this.child_model_views[child_model.id] === undefined) {
305 305 this.child_model_views[child_model.id] = [];
306 306 }
307 307 this.child_model_views[child_model.id].push(child_view.id);
308 308
309 309 // Remember the view by id.
310 310 this.child_views[child_view.id] = child_view;
311 311 return child_view;
312 312 },
313 313
314 314 pop_child_view: function(child_model) {
315 315 // Delete a child view that was previously created using create_child_view.
316 316 var view_ids = this.child_model_views[child_model.id];
317 317 if (view_ids !== undefined) {
318 318
319 319 // Only delete the first view in the list.
320 320 var view_id = view_ids[0];
321 321 var view = this.child_views[view_id];
322 322 delete this.child_views[view_id];
323 323 view_ids.splice(0,1);
324 324 child_model.views.pop(view);
325 325
326 326 // Remove the view list specific to this model if it is empty.
327 327 if (view_ids.length === 0) {
328 328 delete this.child_model_views[child_model.id];
329 329 }
330 330 return view;
331 331 }
332 332 return null;
333 333 },
334 334
335 335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
336 336 // Difference a changed list and call remove and add callbacks for
337 337 // each removed and added item in the new list.
338 338 //
339 339 // Parameters
340 340 // ----------
341 341 // old_list : array
342 342 // new_list : array
343 343 // removed_callback : Callback(item)
344 344 // Callback that is called for each item removed.
345 345 // added_callback : Callback(item)
346 346 // Callback that is called for each item added.
347 347
348 348 // Walk the lists until an unequal entry is found.
349 349 var i;
350 350 for (i = 0; i < new_list.length; i++) {
351 351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
352 352 break;
353 353 }
354 354 }
355 355
356 356 // Remove the non-matching items from the old list.
357 357 for (var j = i; j < old_list.length; j++) {
358 358 removed_callback(old_list[j]);
359 359 }
360 360
361 361 // Add the rest of the new list items.
362 362 for (; i < new_list.length; i++) {
363 363 added_callback(new_list[i]);
364 364 }
365 365 },
366 366
367 367 callbacks: function(){
368 368 // Create msg callbacks for a comm msg.
369 369 return this.model.callbacks(this);
370 370 },
371 371
372 372 render: function(){
373 373 // Render the view.
374 374 //
375 375 // By default, this is only called the first time the view is created
376 376 },
377 377
378 378 show: function(){
379 379 // Show the widget-area
380 380 if (this.options && this.options.cell &&
381 381 this.options.cell.widget_area !== undefined) {
382 382 this.options.cell.widget_area.show();
383 383 }
384 384 },
385 385
386 386 send: function (content) {
387 387 // Send a custom msg associated with this view.
388 388 this.model.send(content, this.callbacks());
389 389 },
390 390
391 391 touch: function () {
392 392 this.model.save_changes(this.callbacks());
393 393 },
394 394
395 395 after_displayed: function (callback, context) {
396 396 // Calls the callback right away is the view is already displayed
397 397 // otherwise, register the callback to the 'displayed' event.
398 398 if (this.is_displayed) {
399 399 callback.apply(context);
400 400 } else {
401 401 this.on('displayed', callback, context);
402 402 }
403 403 },
404 404 });
405 405
406 406
407 407 var DOMWidgetView = WidgetView.extend({
408 408 initialize: function (parameters) {
409 409 // Public constructor
410 410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
411 411 this.on('displayed', this.show, this);
412 this.after_displayed(function() {
413 this.update_visible(this.model, this.model.get("visible"));
414 this.update_css(this.model, this.model.get("_css"));
415 }, this);
416 412 this.model.on('change:visible', this.update_visible, this);
417 413 this.model.on('change:_css', this.update_css, this);
414
418 415 this.model.on('change:_dom_classes', function(model, new_classes) {
419 416 var old_classes = model.previous('children');
420 417 this.update_classes(old_classes, new_classes);
421 418 }, this);
419
422 420 this.model.on('change:fore_color', function (model, value) {
423 421 this.update_attr('color', value); }, this);
422
424 423 this.model.on('change:back_color', function (model, value) {
425 424 this.update_attr('background', value); }, this);
425
426 426 this.model.on('change:width', function (model, value) {
427 427 this.update_attr('width', value); }, this);
428
428 429 this.model.on('change:height', function (model, value) {
429 430 this.update_attr('height', value); }, this);
431
430 432 this.model.on('change:border_color', function (model, value) {
431 433 this.update_attr('border-color', value); }, this);
434
432 435 this.model.on('change:border_width', function (model, value) {
433 436 this.update_attr('border-width', value); }, this);
437
434 438 this.model.on('change:border_style', function (model, value) {
435 439 this.update_attr('border-style', value); }, this);
440
436 441 this.model.on('change:font_style', function (model, value) {
437 442 this.update_attr('font-style', value); }, this);
443
438 444 this.model.on('change:font_weight', function (model, value) {
439 445 this.update_attr('font-weight', value); }, this);
446
440 447 this.model.on('change:font_size', function (model, value) {
441 448 this.update_attr('font-size', value); }, this);
449
442 450 this.model.on('change:font_family', function (model, value) {
443 451 this.update_attr('font-family', value); }, this);
444 this.update_classes([], this.model.get('_dom_classes'));
452
453 this.model.on('change:padding', function (model, value) {
454 this.update_attr('padding', value); }, this);
455
456 this.model.on('change:margin', function (model, value) {
457 this.update_attr('margin', value); }, this);
458
459 this.after_displayed(function() {
460 this.update_visible(this.model, this.model.get("visible"));
461 this.update_css(this.model, this.model.get("_css"));
462
463 this.update_classes([], this.model.get('_dom_classes'));
464 this.update_attr('color', this.model.get('fore_color'));
465 this.update_attr('background', this.model.get('back_color'));
466 this.update_attr('width', this.model.get('width'));
467 this.update_attr('height', this.model.get('height'));
468 this.update_attr('border-color', this.model.get('border_color'));
469 this.update_attr('border-width', this.model.get('border_width'));
470 this.update_attr('border-style', this.model.get('border_style'));
471 this.update_attr('font-style', this.model.get('font_style'));
472 this.update_attr('font-weight', this.model.get('font_weight'));
473 this.update_attr('font-size', this.model.get('font_size'));
474 this.update_attr('font-family', this.model.get('font_family'));
475 this.update_attr('padding', this.model.get('padding'));
476 this.update_attr('margin', this.model.get('margin'));
477 }, this);
445 478 },
446 479
447 480 update_attr: function(name, value) {
448 481 // Set a css attr of the widget view.
449 482 this.$el.css(name, value);
450 483 },
451 484
452 485 update_visible: function(model, value) {
453 486 // Update visibility
454 487 this.$el.toggle(value);
455 488 },
456 489
457 490 update_css: function (model, css) {
458 491 // Update the css styling of this view.
459 492 var e = this.$el;
460 493 if (css === undefined) {return;}
461 494 for (var i = 0; i < css.length; i++) {
462 495 // Apply the css traits to all elements that match the selector.
463 496 var selector = css[i][0];
464 497 var elements = this._get_selector_element(selector);
465 498 if (elements.length > 0) {
466 499 var trait_key = css[i][1];
467 500 var trait_value = css[i][2];
468 501 elements.css(trait_key ,trait_value);
469 502 }
470 503 }
471 504 },
472 505
473 506 update_classes: function (old_classes, new_classes) {
474 507 // Update the DOM classes applied to the topmost element.
475 508 this.do_diff(old_classes, new_classes, function(removed) {
476 509 this.$el.removeClass(removed);
477 510 }, function(added) {
478 511 this.$el.addClass(added);
479 512 });
480 513 },
481 514
482 515 _get_selector_element: function (selector) {
483 516 // Get the elements via the css selector.
484 517 var elements;
485 518 if (!selector) {
486 519 elements = this.$el;
487 520 } else {
488 521 elements = this.$el.find(selector).addBack(selector);
489 522 }
490 523 return elements;
491 524 },
492 525 });
493 526
494 527 var widget = {
495 528 'WidgetModel': WidgetModel,
496 529 'WidgetView': WidgetView,
497 530 'DOMWidgetView': DOMWidgetView,
498 531 };
499 532
500 533 // For backwards compatability.
501 534 $.extend(IPython, widget);
502 535
503 536 return widget;
504 537 });
@@ -1,312 +1,330 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "widgets/js/widget",
6 6 "jqueryui",
7 7 "bootstrap",
8 8 ], function(widget, $){
9 9
10 10 var BoxView = widget.DOMWidgetView.extend({
11 11 initialize: function(){
12 12 // Public constructor
13 13 BoxView.__super__.initialize.apply(this, arguments);
14 14 this.model.on('change:children', function(model, value) {
15 15 this.update_children(model.previous('children'), value);
16 16 }, this);
17 this.model.on('change:overflow_x', function(model, value) {
18 this.update_overflow_x();
19 }, this);
20 this.model.on('change:overflow_y', function(model, value) {
21 this.update_overflow_y();
22 }, this);
17 23 },
18 24
19 25 update_attr: function(name, value) {
20 26 // Set a css attr of the widget view.
21 27 this.$box.css(name, value);
22 28 },
23 29
24 30 render: function(){
25 31 // Called when view is rendered.
26 32 this.$box = this.$el;
27 33 this.$box.addClass('widget-box');
28 34 this.update_children([], this.model.get('children'));
35 this.update_overflow_x();
36 this.update_overflow_y();
37 },
38
39 update_overflow_x: function() {
40 // Called when the x-axis overflow setting is changed.
41 this.$box.css('overflow-x', this.model.get('overflow_x'));
42 },
43
44 update_overflow_y: function() {
45 // Called when the y-axis overflow setting is changed.
46 this.$box.css('overflow-y', this.model.get('overflow_y'));
29 47 },
30 48
31 49 update_children: function(old_list, new_list) {
32 50 // Called when the children list changes.
33 51 this.do_diff(old_list, new_list,
34 52 $.proxy(this.remove_child_model, this),
35 53 $.proxy(this.add_child_model, this));
36 54 },
37 55
38 56 remove_child_model: function(model) {
39 57 // Called when a model is removed from the children list.
40 58 this.pop_child_view(model).remove();
41 59 },
42 60
43 61 add_child_model: function(model) {
44 62 // Called when a model is added to the children list.
45 63 var view = this.create_child_view(model);
46 64 this.$box.append(view.$el);
47 65
48 66 // Trigger the displayed event of the child view.
49 67 this.after_displayed(function() {
50 68 view.trigger('displayed');
51 69 });
52 70 },
53 71 });
54 72
55 73
56 74 var FlexBoxView = BoxView.extend({
57 75 render: function(){
58 76 FlexBoxView.__super__.render.apply(this);
59 77 this.model.on('change:orientation', this.update_orientation, this);
60 78 this.model.on('change:flex', this._flex_changed, this);
61 79 this.model.on('change:pack', this._pack_changed, this);
62 80 this.model.on('change:align', this._align_changed, this);
63 81 this._flex_changed();
64 82 this._pack_changed();
65 83 this._align_changed();
66 84 this.update_orientation();
67 85 },
68 86
69 87 update_orientation: function(){
70 88 var orientation = this.model.get("orientation");
71 89 if (orientation == "vertical") {
72 90 this.$box.removeClass("hbox").addClass("vbox");
73 91 } else {
74 92 this.$box.removeClass("vbox").addClass("hbox");
75 93 }
76 94 },
77 95
78 96 _flex_changed: function(){
79 97 if (this.model.previous('flex')) {
80 98 this.$box.removeClass('box-flex' + this.model.previous('flex'));
81 99 }
82 100 this.$box.addClass('box-flex' + this.model.get('flex'));
83 101 },
84 102
85 103 _pack_changed: function(){
86 104 if (this.model.previous('pack')) {
87 105 this.$box.removeClass(this.model.previous('pack'));
88 106 }
89 107 this.$box.addClass(this.model.get('pack'));
90 108 },
91 109
92 110 _align_changed: function(){
93 111 if (this.model.previous('align')) {
94 112 this.$box.removeClass('align-' + this.model.previous('align'));
95 113 }
96 114 this.$box.addClass('align-' + this.model.get('align'));
97 115 },
98 116 });
99 117
100 118 var PopupView = BoxView.extend({
101 119
102 120 render: function(){
103 121 // Called when view is rendered.
104 122 var that = this;
105 123
106 124 this.$el.on("remove", function(){
107 125 that.$backdrop.remove();
108 126 });
109 127 this.$backdrop = $('<div />')
110 128 .appendTo($('#notebook-container'))
111 129 .addClass('modal-dialog')
112 130 .css('position', 'absolute')
113 131 .css('left', '0px')
114 132 .css('top', '0px');
115 133 this.$window = $('<div />')
116 134 .appendTo(this.$backdrop)
117 135 .addClass('modal-content widget-modal')
118 136 .mousedown(function(){
119 137 that.bring_to_front();
120 138 });
121 139
122 140 // Set the elements array since the this.$window element is not child
123 141 // of this.$el and the parent widget manager or other widgets may
124 142 // need to know about all of the top-level widgets. The IPython
125 143 // widget manager uses this to register the elements with the
126 144 // keyboard manager.
127 145 this.additional_elements = [this.$window];
128 146
129 147 this.$title_bar = $('<div />')
130 148 .addClass('popover-title')
131 149 .appendTo(this.$window)
132 150 .mousedown(function(){
133 151 that.bring_to_front();
134 152 });
135 153 this.$close = $('<button />')
136 154 .addClass('close fa fa-remove')
137 155 .css('margin-left', '5px')
138 156 .appendTo(this.$title_bar)
139 157 .click(function(){
140 158 that.hide();
141 159 event.stopPropagation();
142 160 });
143 161 this.$minimize = $('<button />')
144 162 .addClass('close fa fa-arrow-down')
145 163 .appendTo(this.$title_bar)
146 164 .click(function(){
147 165 that.popped_out = !that.popped_out;
148 166 if (!that.popped_out) {
149 167 that.$minimize
150 168 .removeClass('fa fa-arrow-down')
151 169 .addClass('fa fa-arrow-up');
152 170
153 171 that.$window
154 172 .draggable('destroy')
155 173 .resizable('destroy')
156 174 .removeClass('widget-modal modal-content')
157 175 .addClass('docked-widget-modal')
158 176 .detach()
159 177 .insertBefore(that.$show_button);
160 178 that.$show_button.hide();
161 179 that.$close.hide();
162 180 } else {
163 181 that.$minimize
164 182 .addClass('fa fa-arrow-down')
165 183 .removeClass('fa fa-arrow-up');
166 184
167 185 that.$window
168 186 .removeClass('docked-widget-modal')
169 187 .addClass('widget-modal modal-content')
170 188 .detach()
171 189 .appendTo(that.$backdrop)
172 190 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
173 191 .resizable()
174 192 .children('.ui-resizable-handle').show();
175 193 that.show();
176 194 that.$show_button.show();
177 195 that.$close.show();
178 196 }
179 197 event.stopPropagation();
180 198 });
181 199 this.$title = $('<div />')
182 200 .addClass('widget-modal-title')
183 201 .html("&nbsp;")
184 202 .appendTo(this.$title_bar);
185 203 this.$box = $('<div />')
186 204 .addClass('modal-body')
187 205 .addClass('widget-modal-body')
188 206 .addClass('widget-box')
189 207 .addClass('vbox')
190 208 .appendTo(this.$window);
191 209
192 210 this.$show_button = $('<button />')
193 211 .html("&nbsp;")
194 212 .addClass('btn btn-info widget-modal-show')
195 213 .appendTo(this.$el)
196 214 .click(function(){
197 215 that.show();
198 216 });
199 217
200 218 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
201 219 this.$window.resizable();
202 220 this.$window.on('resize', function(){
203 221 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
204 222 });
205 223
206 224 this._shown_once = false;
207 225 this.popped_out = true;
208 226
209 227 this.update_children([], this.model.get('children'));
210 228 this.model.on('change:children', function(model, value) {
211 229 this.update_children(model.previous('children'), value);
212 230 }, this);
213 231 },
214 232
215 233 hide: function() {
216 234 // Called when the modal hide button is clicked.
217 235 this.$window.hide();
218 236 this.$show_button.removeClass('btn-info');
219 237 },
220 238
221 239 show: function() {
222 240 // Called when the modal show button is clicked.
223 241 this.$show_button.addClass('btn-info');
224 242 this.$window.show();
225 243 if (this.popped_out) {
226 244 this.$window.css("positon", "absolute");
227 245 this.$window.css("top", "0px");
228 246 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
229 247 $(window).scrollLeft()) + "px");
230 248 this.bring_to_front();
231 249 }
232 250 },
233 251
234 252 bring_to_front: function() {
235 253 // Make the modal top-most, z-ordered about the other modals.
236 254 var $widget_modals = $(".widget-modal");
237 255 var max_zindex = 0;
238 256 $widget_modals.each(function (index, el){
239 257 var zindex = parseInt($(el).css('z-index'));
240 258 if (!isNaN(zindex)) {
241 259 max_zindex = Math.max(max_zindex, zindex);
242 260 }
243 261 });
244 262
245 263 // Start z-index of widget modals at 2000
246 264 max_zindex = Math.max(max_zindex, 2000);
247 265
248 266 $widget_modals.each(function (index, el){
249 267 $el = $(el);
250 268 if (max_zindex == parseInt($el.css('z-index'))) {
251 269 $el.css('z-index', max_zindex - 1);
252 270 }
253 271 });
254 272 this.$window.css('z-index', max_zindex);
255 273 },
256 274
257 275 update: function(){
258 276 // Update the contents of this view
259 277 //
260 278 // Called when the model is changed. The model may have been
261 279 // changed by another view or by a state update from the back-end.
262 280 var description = this.model.get('description');
263 281 if (description.trim().length === 0) {
264 282 this.$title.html("&nbsp;"); // Preserve title height
265 283 } else {
266 284 this.$title.text(description);
267 285 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
268 286 }
269 287
270 288 var button_text = this.model.get('button_text');
271 289 if (button_text.trim().length === 0) {
272 290 this.$show_button.html("&nbsp;"); // Preserve button height
273 291 } else {
274 292 this.$show_button.text(button_text);
275 293 }
276 294
277 295 if (!this._shown_once) {
278 296 this._shown_once = true;
279 297 this.show();
280 298 }
281 299
282 300 return PopupView.__super__.update.apply(this);
283 301 },
284 302
285 303 _get_selector_element: function(selector) {
286 304 // Get an element view a 'special' jquery selector. (see widget.js)
287 305 //
288 306 // Since the modal actually isn't within the $el in the DOM, we need to extend
289 307 // the selector logic to allow the user to set css on the modal if need be.
290 308 // The convention used is:
291 309 // "modal" - select the modal div
292 310 // "modal [selector]" - select element(s) within the modal div.
293 311 // "[selector]" - select elements within $el
294 312 // "" - select the $el
295 313 if (selector.substring(0, 5) == 'modal') {
296 314 if (selector == 'modal') {
297 315 return this.$window;
298 316 } else {
299 317 return this.$window.find(selector.substring(6));
300 318 }
301 319 } else {
302 320 return PopupView.__super__._get_selector_element.apply(this, [selector]);
303 321 }
304 322 },
305 323 });
306 324
307 325 return {
308 326 'BoxView': BoxView,
309 327 'PopupView': PopupView,
310 328 'FlexBoxView': FlexBoxView,
311 329 };
312 330 });
@@ -1,425 +1,427 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16 import collections
17 17
18 18 from IPython.core.getipython import get_ipython
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 22 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 23 from IPython.utils.py3compat import string_types
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes
27 27 #-----------------------------------------------------------------------------
28 28 class CallbackDispatcher(LoggingConfigurable):
29 29 """A structure for registering and running callbacks"""
30 30 callbacks = List()
31 31
32 32 def __call__(self, *args, **kwargs):
33 33 """Call all of the registered callbacks."""
34 34 value = None
35 35 for callback in self.callbacks:
36 36 try:
37 37 local_value = callback(*args, **kwargs)
38 38 except Exception as e:
39 39 ip = get_ipython()
40 40 if ip is None:
41 41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 42 else:
43 43 ip.showtraceback()
44 44 else:
45 45 value = local_value if local_value is not None else value
46 46 return value
47 47
48 48 def register_callback(self, callback, remove=False):
49 49 """(Un)Register a callback
50 50
51 51 Parameters
52 52 ----------
53 53 callback: method handle
54 54 Method to be registered or unregistered.
55 55 remove=False: bool
56 56 Whether to unregister the callback."""
57 57
58 58 # (Un)Register the callback.
59 59 if remove and callback in self.callbacks:
60 60 self.callbacks.remove(callback)
61 61 elif not remove and callback not in self.callbacks:
62 62 self.callbacks.append(callback)
63 63
64 64 def _show_traceback(method):
65 65 """decorator for showing tracebacks in IPython"""
66 66 def m(self, *args, **kwargs):
67 67 try:
68 68 return(method(self, *args, **kwargs))
69 69 except Exception as e:
70 70 ip = get_ipython()
71 71 if ip is None:
72 72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 73 else:
74 74 ip.showtraceback()
75 75 return m
76 76
77 77 class Widget(LoggingConfigurable):
78 78 #-------------------------------------------------------------------------
79 79 # Class attributes
80 80 #-------------------------------------------------------------------------
81 81 _widget_construction_callback = None
82 82 widgets = {}
83 83
84 84 @staticmethod
85 85 def on_widget_constructed(callback):
86 86 """Registers a callback to be called when a widget is constructed.
87 87
88 88 The callback must have the following signature:
89 89 callback(widget)"""
90 90 Widget._widget_construction_callback = callback
91 91
92 92 @staticmethod
93 93 def _call_widget_constructed(widget):
94 94 """Static method, called when a widget is constructed."""
95 95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 96 Widget._widget_construction_callback(widget)
97 97
98 98 #-------------------------------------------------------------------------
99 99 # Traits
100 100 #-------------------------------------------------------------------------
101 101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
102 102 registered in the front-end to create and sync this widget with.""")
103 103 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
104 104 to use to represent the widget.""", sync=True)
105 105 comm = Instance('IPython.kernel.comm.Comm')
106 106
107 107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
108 108 front-end can send before receiving an idle msg from the back-end.""")
109 109
110 110 keys = List()
111 111 def _keys_default(self):
112 112 return [name for name in self.traits(sync=True)]
113 113
114 114 _property_lock = Tuple((None, None))
115 115 _send_state_lock = Int(0)
116 116 _states_to_send = Set(allow_none=False)
117 117 _display_callbacks = Instance(CallbackDispatcher, ())
118 118 _msg_callbacks = Instance(CallbackDispatcher, ())
119 119
120 120 #-------------------------------------------------------------------------
121 121 # (Con/de)structor
122 122 #-------------------------------------------------------------------------
123 123 def __init__(self, **kwargs):
124 124 """Public constructor"""
125 125 self._model_id = kwargs.pop('model_id', None)
126 126 super(Widget, self).__init__(**kwargs)
127 127
128 128 self.on_trait_change(self._handle_property_changed, self.keys)
129 129 Widget._call_widget_constructed(self)
130 130 self.open()
131 131
132 132 def __del__(self):
133 133 """Object disposal"""
134 134 self.close()
135 135
136 136 #-------------------------------------------------------------------------
137 137 # Properties
138 138 #-------------------------------------------------------------------------
139 139
140 140 def open(self):
141 141 """Open a comm to the frontend if one isn't already open."""
142 142 if self.comm is None:
143 143 if self._model_id is None:
144 144 self.comm = Comm(target_name=self._model_name)
145 145 self._model_id = self.model_id
146 146 else:
147 147 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
148 148 self.comm.on_msg(self._handle_msg)
149 149 Widget.widgets[self.model_id] = self
150 150
151 151 # first update
152 152 self.send_state()
153 153
154 154 @property
155 155 def model_id(self):
156 156 """Gets the model id of this widget.
157 157
158 158 If a Comm doesn't exist yet, a Comm will be created automagically."""
159 159 return self.comm.comm_id
160 160
161 161 #-------------------------------------------------------------------------
162 162 # Methods
163 163 #-------------------------------------------------------------------------
164 164
165 165 def close(self):
166 166 """Close method.
167 167
168 168 Closes the underlying comm.
169 169 When the comm is closed, all of the widget views are automatically
170 170 removed from the front-end."""
171 171 if self.comm is not None:
172 172 Widget.widgets.pop(self.model_id, None)
173 173 self.comm.close()
174 174 self.comm = None
175 175
176 176 def send_state(self, key=None):
177 177 """Sends the widget state, or a piece of it, to the front-end.
178 178
179 179 Parameters
180 180 ----------
181 181 key : unicode, or iterable (optional)
182 182 A single property's name or iterable of property names to sync with the front-end.
183 183 """
184 184 self._send({
185 185 "method" : "update",
186 186 "state" : self.get_state(key=key)
187 187 })
188 188
189 189 def get_state(self, key=None):
190 190 """Gets the widget state, or a piece of it.
191 191
192 192 Parameters
193 193 ----------
194 194 key : unicode or iterable (optional)
195 195 A single property's name or iterable of property names to get.
196 196 """
197 197 if key is None:
198 198 keys = self.keys
199 199 elif isinstance(key, string_types):
200 200 keys = [key]
201 201 elif isinstance(key, collections.Iterable):
202 202 keys = key
203 203 else:
204 204 raise ValueError("key must be a string, an iterable of keys, or None")
205 205 state = {}
206 206 for k in keys:
207 207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
208 208 value = getattr(self, k)
209 209 state[k] = f(value)
210 210 return state
211 211
212 212 def send(self, content):
213 213 """Sends a custom msg to the widget model in the front-end.
214 214
215 215 Parameters
216 216 ----------
217 217 content : dict
218 218 Content of the message to send.
219 219 """
220 220 self._send({"method": "custom", "content": content})
221 221
222 222 def on_msg(self, callback, remove=False):
223 223 """(Un)Register a custom msg receive callback.
224 224
225 225 Parameters
226 226 ----------
227 227 callback: callable
228 228 callback will be passed two arguments when a message arrives::
229 229
230 230 callback(widget, content)
231 231
232 232 remove: bool
233 233 True if the callback should be unregistered."""
234 234 self._msg_callbacks.register_callback(callback, remove=remove)
235 235
236 236 def on_displayed(self, callback, remove=False):
237 237 """(Un)Register a widget displayed callback.
238 238
239 239 Parameters
240 240 ----------
241 241 callback: method handler
242 242 Must have a signature of::
243 243
244 244 callback(widget, **kwargs)
245 245
246 246 kwargs from display are passed through without modification.
247 247 remove: bool
248 248 True if the callback should be unregistered."""
249 249 self._display_callbacks.register_callback(callback, remove=remove)
250 250
251 251 #-------------------------------------------------------------------------
252 252 # Support methods
253 253 #-------------------------------------------------------------------------
254 254 @contextmanager
255 255 def _lock_property(self, key, value):
256 256 """Lock a property-value pair.
257 257
258 258 The value should be the JSON state of the property.
259 259
260 260 NOTE: This, in addition to the single lock for all state changes, is
261 261 flawed. In the future we may want to look into buffering state changes
262 262 back to the front-end."""
263 263 self._property_lock = (key, value)
264 264 try:
265 265 yield
266 266 finally:
267 267 self._property_lock = (None, None)
268 268
269 269 @contextmanager
270 270 def hold_sync(self):
271 271 """Hold syncing any state until the context manager is released"""
272 272 # We increment a value so that this can be nested. Syncing will happen when
273 273 # all levels have been released.
274 274 self._send_state_lock += 1
275 275 try:
276 276 yield
277 277 finally:
278 278 self._send_state_lock -=1
279 279 if self._send_state_lock == 0:
280 280 self.send_state(self._states_to_send)
281 281 self._states_to_send.clear()
282 282
283 283 def _should_send_property(self, key, value):
284 284 """Check the property lock (property_lock)"""
285 285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
286 286 if (key == self._property_lock[0]
287 287 and to_json(value) == self._property_lock[1]):
288 288 return False
289 289 elif self._send_state_lock > 0:
290 290 self._states_to_send.add(key)
291 291 return False
292 292 else:
293 293 return True
294 294
295 295 # Event handlers
296 296 @_show_traceback
297 297 def _handle_msg(self, msg):
298 298 """Called when a msg is received from the front-end"""
299 299 data = msg['content']['data']
300 300 method = data['method']
301 301 if not method in ['backbone', 'custom']:
302 302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
303 303
304 304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
305 305 if method == 'backbone' and 'sync_data' in data:
306 306 sync_data = data['sync_data']
307 307 self._handle_receive_state(sync_data) # handles all methods
308 308
309 309 # Handle a custom msg from the front-end
310 310 elif method == 'custom':
311 311 if 'content' in data:
312 312 self._handle_custom_msg(data['content'])
313 313
314 314 def _handle_receive_state(self, sync_data):
315 315 """Called when a state is received from the front-end."""
316 316 for name in self.keys:
317 317 if name in sync_data:
318 318 json_value = sync_data[name]
319 319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
320 320 with self._lock_property(name, json_value):
321 321 setattr(self, name, from_json(json_value))
322 322
323 323 def _handle_custom_msg(self, content):
324 324 """Called when a custom msg is received."""
325 325 self._msg_callbacks(self, content)
326 326
327 327 def _handle_property_changed(self, name, old, new):
328 328 """Called when a property has been changed."""
329 329 # Make sure this isn't information that the front-end just sent us.
330 330 if self._should_send_property(name, new):
331 331 # Send new state to front-end
332 332 self.send_state(key=name)
333 333
334 334 def _handle_displayed(self, **kwargs):
335 335 """Called when a view has been displayed for this widget instance"""
336 336 self._display_callbacks(self, **kwargs)
337 337
338 338 def _trait_to_json(self, x):
339 339 """Convert a trait value to json
340 340
341 341 Traverse lists/tuples and dicts and serialize their values as well.
342 342 Replace any widgets with their model_id
343 343 """
344 344 if isinstance(x, dict):
345 345 return {k: self._trait_to_json(v) for k, v in x.items()}
346 346 elif isinstance(x, (list, tuple)):
347 347 return [self._trait_to_json(v) for v in x]
348 348 elif isinstance(x, Widget):
349 349 return "IPY_MODEL_" + x.model_id
350 350 else:
351 351 return x # Value must be JSON-able
352 352
353 353 def _trait_from_json(self, x):
354 354 """Convert json values to objects
355 355
356 356 Replace any strings representing valid model id values to Widget references.
357 357 """
358 358 if isinstance(x, dict):
359 359 return {k: self._trait_from_json(v) for k, v in x.items()}
360 360 elif isinstance(x, (list, tuple)):
361 361 return [self._trait_from_json(v) for v in x]
362 362 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
363 363 # we want to support having child widgets at any level in a hierarchy
364 364 # trusting that a widget UUID will not appear out in the wild
365 365 return Widget.widgets[x]
366 366 else:
367 367 return x
368 368
369 369 def _ipython_display_(self, **kwargs):
370 370 """Called when `IPython.display.display` is called on the widget."""
371 371 # Show view. By sending a display message, the comm is opened and the
372 372 # initial state is sent.
373 373 self._send({"method": "display"})
374 374 self._handle_displayed(**kwargs)
375 375
376 376 def _send(self, msg):
377 377 """Sends a message to the model in the front-end."""
378 378 self.comm.send(msg)
379 379
380 380
381 381 class DOMWidget(Widget):
382 382 visible = Bool(True, help="Whether the widget is visible.", sync=True)
383 383 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
384 384 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
385 385
386 386 width = CUnicode(sync=True)
387 387 height = CUnicode(sync=True)
388 padding = CUnicode(sync=True)
389 margin = CUnicode(sync=True)
388 390
389 391 fore_color = Unicode(sync=True)
390 392 back_color = Unicode(sync=True)
391 393 border_color = Unicode(sync=True)
392 394
393 395 border_width = CUnicode(sync=True)
394 396 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
395 397 'none',
396 398 'hidden',
397 399 'dotted',
398 400 'dashed',
399 401 'solid',
400 402 'double',
401 403 'groove',
402 404 'ridge',
403 405 'inset',
404 406 'outset',
405 407 'initial',
406 408 'inherit', ''],
407 409 default_value='', sync=True)
408 410
409 411 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
410 412 'normal',
411 413 'italic',
412 414 'oblique',
413 415 'initial',
414 416 'inherit', ''],
415 417 default_value='', sync=True)
416 418 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
417 419 'normal',
418 420 'bold',
419 421 'bolder',
420 422 'lighter',
421 423 'initial',
422 424 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
423 425 default_value='', sync=True)
424 426 font_size = CUnicode(sync=True)
425 427 font_family = Unicode(sync=True)
@@ -1,73 +1,83 b''
1 1 """Box class.
2 2
3 3 Represents a container that can be used to group other widgets.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 from .widget import DOMWidget
10 10 from IPython.utils.traitlets import Unicode, Tuple, TraitError, Int, CaselessStrEnum
11 11 from IPython.utils.warn import DeprecatedClass
12 12
13 13 class Box(DOMWidget):
14 14 """Displays multiple widgets in a group."""
15 15 _view_name = Unicode('BoxView', sync=True)
16 16
17 17 # Child widgets in the container.
18 18 # Using a tuple here to force reassignment to update the list.
19 19 # When a proper notifying-list trait exists, that is what should be used here.
20 20 children = Tuple(sync=True, allow_none=False)
21
22 _overflow_values = ['visible', 'hidden', 'scroll', 'auto', 'initial', 'inherit', '']
23 overflow_x = CaselessStrEnum(
24 values=_overflow_values,
25 default_value='', allow_none=False, sync=True, help="""Specifies what
26 happens to content that is too large for the rendered region.""")
27 overflow_y = CaselessStrEnum(
28 values=_overflow_values,
29 default_value='', allow_none=False, sync=True, help="""Specifies what
30 happens to content that is too large for the rendered region.""")
21 31
22 32 def __init__(self, children = (), **kwargs):
23 33 kwargs['children'] = children
24 34 super(Box, self).__init__(**kwargs)
25 35 self.on_displayed(Box._fire_children_displayed)
26 36
27 37 def _fire_children_displayed(self):
28 38 for child in self.children:
29 39 child._handle_displayed()
30 40
31 41
32 42 class Popup(Box):
33 43 """Displays multiple widgets in an in page popup div."""
34 44 _view_name = Unicode('PopupView', sync=True)
35 45
36 46 description = Unicode(sync=True)
37 47 button_text = Unicode(sync=True)
38 48
39 49
40 50 class FlexBox(Box):
41 51 """Displays multiple widgets using the flexible box model."""
42 52 _view_name = Unicode('FlexBoxView', sync=True)
43 53 orientation = CaselessStrEnum(values=['vertical', 'horizontal'], default_value='vertical', sync=True)
44 54 flex = Int(0, sync=True, help="""Specify the flexible-ness of the model.""")
45 55 def _flex_changed(self, name, old, new):
46 56 new = min(max(0, new), 2)
47 57 if self.flex != new:
48 58 self.flex = new
49 59
50 60 _locations = ['start', 'center', 'end', 'baseline', 'stretch']
51 61 pack = CaselessStrEnum(
52 62 values=_locations,
53 63 default_value='start', allow_none=False, sync=True)
54 64 align = CaselessStrEnum(
55 65 values=_locations,
56 66 default_value='start', allow_none=False, sync=True)
57 67
58 68
59 69 def VBox(*pargs, **kwargs):
60 70 """Displays multiple widgets vertically using the flexible box model."""
61 71 kwargs['orientation'] = 'vertical'
62 72 return FlexBox(*pargs, **kwargs)
63 73
64 74 def HBox(*pargs, **kwargs):
65 75 """Displays multiple widgets horizontally using the flexible box model."""
66 76 kwargs['orientation'] = 'horizontal'
67 77 return FlexBox(*pargs, **kwargs)
68 78
69 79
70 80 # Remove in IPython 4.0
71 81 ContainerWidget = DeprecatedClass(Box, 'ContainerWidget')
72 82 PopupWidget = DeprecatedClass(Popup, 'PopupWidget')
73 83
General Comments 0
You need to be logged in to leave comments. Login now