##// END OF EJS Templates
Address problems found in in-person review
Jonathan Frederic -
Show More
@@ -1,576 +1,588 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 412 this.model.on('change:visible', this.update_visible, this);
413 413 this.model.on('change:_css', this.update_css, this);
414 414
415 415 this.model.on('change:_dom_classes', function(model, new_classes) {
416 416 var old_classes = model.previous('_dom_classes');
417 417 this.update_classes(old_classes, new_classes);
418 418 }, this);
419 419
420 420 this.model.on('change:color', function (model, value) {
421 421 this.update_attr('color', value); }, this);
422 422
423 423 this.model.on('change:background_color', function (model, value) {
424 424 this.update_attr('background', value); }, this);
425 425
426 426 this.model.on('change:width', function (model, value) {
427 427 this.update_attr('width', value); }, this);
428 428
429 429 this.model.on('change:height', function (model, value) {
430 430 this.update_attr('height', value); }, this);
431 431
432 432 this.model.on('change:border_color', function (model, value) {
433 433 this.update_attr('border-color', value); }, this);
434 434
435 435 this.model.on('change:border_width', function (model, value) {
436 436 this.update_attr('border-width', value); }, this);
437 437
438 438 this.model.on('change:border_style', function (model, value) {
439 439 this.update_attr('border-style', value); }, this);
440 440
441 441 this.model.on('change:font_style', function (model, value) {
442 442 this.update_attr('font-style', value); }, this);
443 443
444 444 this.model.on('change:font_weight', function (model, value) {
445 445 this.update_attr('font-weight', value); }, this);
446 446
447 447 this.model.on('change:font_size', function (model, value) {
448 this.update_attr('font-size', value); }, this);
448 this.update_attr('font-size', this._default_px(value)); }, this);
449 449
450 450 this.model.on('change:font_family', function (model, value) {
451 451 this.update_attr('font-family', value); }, this);
452 452
453 453 this.model.on('change:padding', function (model, value) {
454 454 this.update_attr('padding', value); }, this);
455 455
456 456 this.model.on('change:margin', function (model, value) {
457 this.update_attr('margin', value); }, this);
457 this.update_attr('margin', this._default_px(value)); }, this);
458
459 this.model.on('change:border_radius', function (model, value) {
460 this.update_attr('border-radius', this._default_px(value)); }, this);
458 461
459 462 this.after_displayed(function() {
460 463 this.update_visible(this.model, this.model.get("visible"));
461 464 this.update_css(this.model, this.model.get("_css"));
462 465
463 466 this.update_classes([], this.model.get('_dom_classes'));
464 467 this.update_attr('color', this.model.get('color'));
465 468 this.update_attr('background', this.model.get('background_color'));
466 469 this.update_attr('width', this.model.get('width'));
467 470 this.update_attr('height', this.model.get('height'));
468 471 this.update_attr('border-color', this.model.get('border_color'));
469 472 this.update_attr('border-width', this.model.get('border_width'));
470 473 this.update_attr('border-style', this.model.get('border_style'));
471 474 this.update_attr('font-style', this.model.get('font_style'));
472 475 this.update_attr('font-weight', this.model.get('font_weight'));
473 476 this.update_attr('font-size', this.model.get('font_size'));
474 477 this.update_attr('font-family', this.model.get('font_family'));
475 478 this.update_attr('padding', this.model.get('padding'));
476 479 this.update_attr('margin', this.model.get('margin'));
480 this.update_attr('border-radius', this.model.get('border_radius'));
477 481 }, this);
478 482 },
479 483
484 _default_px: function(value) {
485 // Makes browser interpret a numerical string as a pixel value.
486 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
487 return value.trim() + 'px';
488 }
489 return value;
490 },
491
480 492 update_attr: function(name, value) {
481 493 // Set a css attr of the widget view.
482 494 this.$el.css(name, value);
483 495 },
484 496
485 497 update_visible: function(model, value) {
486 498 // Update visibility
487 499 this.$el.toggle(value);
488 500 },
489 501
490 502 update_css: function (model, css) {
491 503 // Update the css styling of this view.
492 504 var e = this.$el;
493 505 if (css === undefined) {return;}
494 506 for (var i = 0; i < css.length; i++) {
495 507 // Apply the css traits to all elements that match the selector.
496 508 var selector = css[i][0];
497 509 var elements = this._get_selector_element(selector);
498 510 if (elements.length > 0) {
499 511 var trait_key = css[i][1];
500 512 var trait_value = css[i][2];
501 513 elements.css(trait_key ,trait_value);
502 514 }
503 515 }
504 516 },
505 517
506 518 update_classes: function (old_classes, new_classes, $el) {
507 519 // Update the DOM classes applied to an element, default to this.$el.
508 520 if ($el===undefined) {
509 521 $el = this.$el;
510 522 }
511 523 this.do_diff(old_classes, new_classes, function(removed) {
512 524 $el.removeClass(removed);
513 525 }, function(added) {
514 526 $el.addClass(added);
515 527 });
516 528 },
517 529
518 530 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
519 531 // Update the DOM classes applied to the widget based on a single
520 532 // trait's value.
521 533 //
522 534 // Given a trait value classes map, this function automatically
523 535 // handles applying the appropriate classes to the widget element
524 536 // and removing classes that are no longer valid.
525 537 //
526 538 // Parameters
527 539 // ----------
528 540 // class_map: dictionary
529 541 // Dictionary of trait values to class lists.
530 542 // Example:
531 543 // {
532 544 // success: ['alert', 'alert-success'],
533 545 // info: ['alert', 'alert-info'],
534 546 // warning: ['alert', 'alert-warning'],
535 547 // danger: ['alert', 'alert-danger']
536 548 // };
537 549 // trait_name: string
538 550 // Name of the trait to check the value of.
539 551 // previous_trait_value: optional string, default ''
540 552 // Last trait value
541 553 // $el: optional jQuery element handle, defaults to this.$el
542 554 // Element that the classes are applied to.
543 555 var key = previous_trait_value;
544 556 if (key === undefined) {
545 557 key = this.model.previous(trait_name);
546 558 }
547 559 var old_classes = class_map[key] ? class_map[key] : [];
548 560 key = this.model.get(trait_name);
549 561 var new_classes = class_map[key] ? class_map[key] : [];
550 562
551 563 this.update_classes(old_classes, new_classes, $el || this.$el);
552 564 },
553 565
554 566 _get_selector_element: function (selector) {
555 567 // Get the elements via the css selector.
556 568 var elements;
557 569 if (!selector) {
558 570 elements = this.$el;
559 571 } else {
560 572 elements = this.$el.find(selector).addBack(selector);
561 573 }
562 574 return elements;
563 575 },
564 576 });
565 577
566 578 var widget = {
567 579 'WidgetModel': WidgetModel,
568 580 'WidgetView': WidgetView,
569 581 'DOMWidgetView': DOMWidgetView,
570 582 };
571 583
572 584 // For backwards compatability.
573 585 $.extend(IPython, widget);
574 586
575 587 return widget;
576 588 });
@@ -1,344 +1,344 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 17 this.model.on('change:overflow_x', function(model, value) {
18 18 this.update_overflow_x();
19 19 }, this);
20 20 this.model.on('change:overflow_y', function(model, value) {
21 21 this.update_overflow_y();
22 22 }, this);
23 23 this.model.on('change:box_style', function(model, value) {
24 24 this.update_box_style();
25 25 }, this);
26 26 },
27 27
28 28 update_attr: function(name, value) {
29 29 // Set a css attr of the widget view.
30 30 this.$box.css(name, value);
31 31 },
32 32
33 33 render: function(){
34 34 // Called when view is rendered.
35 35 this.$box = this.$el;
36 36 this.$box.addClass('widget-box');
37 37 this.update_children([], this.model.get('children'));
38 38 this.update_overflow_x();
39 39 this.update_overflow_y();
40 40 this.update_box_style('');
41 41 },
42 42
43 43 update_overflow_x: function() {
44 44 // Called when the x-axis overflow setting is changed.
45 45 this.$box.css('overflow-x', this.model.get('overflow_x'));
46 46 },
47 47
48 48 update_overflow_y: function() {
49 49 // Called when the y-axis overflow setting is changed.
50 50 this.$box.css('overflow-y', this.model.get('overflow_y'));
51 51 },
52 52
53 53 update_box_style: function(previous_trait_value) {
54 54 var class_map = {
55 55 success: ['alert', 'alert-success'],
56 56 info: ['alert', 'alert-info'],
57 57 warning: ['alert', 'alert-warning'],
58 58 danger: ['alert', 'alert-danger']
59 59 };
60 60 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
61 61 },
62 62
63 63 update_children: function(old_list, new_list) {
64 64 // Called when the children list changes.
65 65 this.do_diff(old_list, new_list,
66 66 $.proxy(this.remove_child_model, this),
67 67 $.proxy(this.add_child_model, this));
68 68 },
69 69
70 70 remove_child_model: function(model) {
71 71 // Called when a model is removed from the children list.
72 72 this.pop_child_view(model).remove();
73 73 },
74 74
75 75 add_child_model: function(model) {
76 76 // Called when a model is added to the children list.
77 77 var view = this.create_child_view(model);
78 78 this.$box.append(view.$el);
79 79
80 80 // Trigger the displayed event of the child view.
81 81 this.after_displayed(function() {
82 82 view.trigger('displayed');
83 83 });
84 84 },
85 85 });
86 86
87 87
88 88 var FlexBoxView = BoxView.extend({
89 89 render: function(){
90 90 FlexBoxView.__super__.render.apply(this);
91 91 this.model.on('change:orientation', this.update_orientation, this);
92 92 this.model.on('change:flex', this._flex_changed, this);
93 93 this.model.on('change:pack', this._pack_changed, this);
94 94 this.model.on('change:align', this._align_changed, this);
95 95 this._flex_changed();
96 96 this._pack_changed();
97 97 this._align_changed();
98 98 this.update_orientation();
99 99 },
100 100
101 101 update_orientation: function(){
102 102 var orientation = this.model.get("orientation");
103 103 if (orientation == "vertical") {
104 104 this.$box.removeClass("hbox").addClass("vbox");
105 105 } else {
106 106 this.$box.removeClass("vbox").addClass("hbox");
107 107 }
108 108 },
109 109
110 110 _flex_changed: function(){
111 111 if (this.model.previous('flex')) {
112 112 this.$box.removeClass('box-flex' + this.model.previous('flex'));
113 113 }
114 114 this.$box.addClass('box-flex' + this.model.get('flex'));
115 115 },
116 116
117 117 _pack_changed: function(){
118 118 if (this.model.previous('pack')) {
119 119 this.$box.removeClass(this.model.previous('pack'));
120 120 }
121 121 this.$box.addClass(this.model.get('pack'));
122 122 },
123 123
124 124 _align_changed: function(){
125 125 if (this.model.previous('align')) {
126 126 this.$box.removeClass('align-' + this.model.previous('align'));
127 127 }
128 128 this.$box.addClass('align-' + this.model.get('align'));
129 129 },
130 130 });
131 131
132 132 var PopupView = BoxView.extend({
133 133
134 134 render: function(){
135 135 // Called when view is rendered.
136 136 var that = this;
137 137
138 138 this.$el.on("remove", function(){
139 139 that.$backdrop.remove();
140 140 });
141 141 this.$backdrop = $('<div />')
142 142 .appendTo($('#notebook-container'))
143 143 .addClass('modal-dialog')
144 144 .css('position', 'absolute')
145 145 .css('left', '0px')
146 146 .css('top', '0px');
147 147 this.$window = $('<div />')
148 148 .appendTo(this.$backdrop)
149 149 .addClass('modal-content widget-modal')
150 150 .mousedown(function(){
151 151 that.bring_to_front();
152 152 });
153 153
154 154 // Set the elements array since the this.$window element is not child
155 155 // of this.$el and the parent widget manager or other widgets may
156 156 // need to know about all of the top-level widgets. The IPython
157 157 // widget manager uses this to register the elements with the
158 158 // keyboard manager.
159 159 this.additional_elements = [this.$window];
160 160
161 161 this.$title_bar = $('<div />')
162 162 .addClass('popover-title')
163 163 .appendTo(this.$window)
164 164 .mousedown(function(){
165 165 that.bring_to_front();
166 166 });
167 167 this.$close = $('<button />')
168 168 .addClass('close fa fa-remove')
169 169 .css('margin-left', '5px')
170 170 .appendTo(this.$title_bar)
171 171 .click(function(){
172 172 that.hide();
173 173 event.stopPropagation();
174 174 });
175 175 this.$minimize = $('<button />')
176 176 .addClass('close fa fa-arrow-down')
177 177 .appendTo(this.$title_bar)
178 178 .click(function(){
179 179 that.popped_out = !that.popped_out;
180 180 if (!that.popped_out) {
181 181 that.$minimize
182 .removeClass('fa fa-arrow-down')
183 .addClass('fa fa-arrow-up');
182 .removeClass('fa-arrow-down')
183 .addClass('fa-arrow-up');
184 184
185 185 that.$window
186 186 .draggable('destroy')
187 187 .resizable('destroy')
188 188 .removeClass('widget-modal modal-content')
189 189 .addClass('docked-widget-modal')
190 190 .detach()
191 191 .insertBefore(that.$show_button);
192 192 that.$show_button.hide();
193 193 that.$close.hide();
194 194 } else {
195 195 that.$minimize
196 .addClass('fa fa-arrow-down')
197 .removeClass('fa fa-arrow-up');
196 .addClass('fa-arrow-down')
197 .removeClass('fa-arrow-up');
198 198
199 199 that.$window
200 200 .removeClass('docked-widget-modal')
201 201 .addClass('widget-modal modal-content')
202 202 .detach()
203 203 .appendTo(that.$backdrop)
204 204 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
205 205 .resizable()
206 206 .children('.ui-resizable-handle').show();
207 207 that.show();
208 208 that.$show_button.show();
209 209 that.$close.show();
210 210 }
211 211 event.stopPropagation();
212 212 });
213 213 this.$title = $('<div />')
214 214 .addClass('widget-modal-title')
215 215 .html("&nbsp;")
216 216 .appendTo(this.$title_bar);
217 217 this.$box = $('<div />')
218 218 .addClass('modal-body')
219 219 .addClass('widget-modal-body')
220 220 .addClass('widget-box')
221 221 .addClass('vbox')
222 222 .appendTo(this.$window);
223 223
224 224 this.$show_button = $('<button />')
225 225 .html("&nbsp;")
226 226 .addClass('btn btn-info widget-modal-show')
227 227 .appendTo(this.$el)
228 228 .click(function(){
229 229 that.show();
230 230 });
231 231
232 232 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
233 233 this.$window.resizable();
234 234 this.$window.on('resize', function(){
235 235 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
236 236 });
237 237
238 238 this._shown_once = false;
239 239 this.popped_out = true;
240 240
241 241 this.update_children([], this.model.get('children'));
242 242 this.model.on('change:children', function(model, value) {
243 243 this.update_children(model.previous('children'), value);
244 244 }, this);
245 245 },
246 246
247 247 hide: function() {
248 248 // Called when the modal hide button is clicked.
249 249 this.$window.hide();
250 250 this.$show_button.removeClass('btn-info');
251 251 },
252 252
253 253 show: function() {
254 254 // Called when the modal show button is clicked.
255 255 this.$show_button.addClass('btn-info');
256 256 this.$window.show();
257 257 if (this.popped_out) {
258 258 this.$window.css("positon", "absolute");
259 259 this.$window.css("top", "0px");
260 260 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
261 261 $(window).scrollLeft()) + "px");
262 262 this.bring_to_front();
263 263 }
264 264 },
265 265
266 266 bring_to_front: function() {
267 267 // Make the modal top-most, z-ordered about the other modals.
268 268 var $widget_modals = $(".widget-modal");
269 269 var max_zindex = 0;
270 270 $widget_modals.each(function (index, el){
271 271 var zindex = parseInt($(el).css('z-index'));
272 272 if (!isNaN(zindex)) {
273 273 max_zindex = Math.max(max_zindex, zindex);
274 274 }
275 275 });
276 276
277 277 // Start z-index of widget modals at 2000
278 278 max_zindex = Math.max(max_zindex, 2000);
279 279
280 280 $widget_modals.each(function (index, el){
281 281 $el = $(el);
282 282 if (max_zindex == parseInt($el.css('z-index'))) {
283 283 $el.css('z-index', max_zindex - 1);
284 284 }
285 285 });
286 286 this.$window.css('z-index', max_zindex);
287 287 },
288 288
289 289 update: function(){
290 290 // Update the contents of this view
291 291 //
292 292 // Called when the model is changed. The model may have been
293 293 // changed by another view or by a state update from the back-end.
294 294 var description = this.model.get('description');
295 295 if (description.trim().length === 0) {
296 296 this.$title.html("&nbsp;"); // Preserve title height
297 297 } else {
298 298 this.$title.text(description);
299 299 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
300 300 }
301 301
302 302 var button_text = this.model.get('button_text');
303 303 if (button_text.trim().length === 0) {
304 304 this.$show_button.html("&nbsp;"); // Preserve button height
305 305 } else {
306 306 this.$show_button.text(button_text);
307 307 }
308 308
309 309 if (!this._shown_once) {
310 310 this._shown_once = true;
311 311 this.show();
312 312 }
313 313
314 314 return PopupView.__super__.update.apply(this);
315 315 },
316 316
317 317 _get_selector_element: function(selector) {
318 318 // Get an element view a 'special' jquery selector. (see widget.js)
319 319 //
320 320 // Since the modal actually isn't within the $el in the DOM, we need to extend
321 321 // the selector logic to allow the user to set css on the modal if need be.
322 322 // The convention used is:
323 323 // "modal" - select the modal div
324 324 // "modal [selector]" - select element(s) within the modal div.
325 325 // "[selector]" - select elements within $el
326 326 // "" - select the $el
327 327 if (selector.substring(0, 5) == 'modal') {
328 328 if (selector == 'modal') {
329 329 return this.$window;
330 330 } else {
331 331 return this.$window.find(selector.substring(6));
332 332 }
333 333 } else {
334 334 return PopupView.__super__._get_selector_element.apply(this, [selector]);
335 335 }
336 336 },
337 337 });
338 338
339 339 return {
340 340 'BoxView': BoxView,
341 341 'PopupView': PopupView,
342 342 'FlexBoxView': FlexBoxView,
343 343 };
344 344 });
@@ -1,427 +1,439 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 388 padding = CUnicode(sync=True)
389 389 margin = CUnicode(sync=True)
390 390
391 391 color = Unicode(sync=True)
392 392 background_color = Unicode(sync=True)
393 393 border_color = Unicode(sync=True)
394 394
395 395 border_width = CUnicode(sync=True)
396 border_radius = CUnicode(sync=True)
396 397 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
397 398 'none',
398 399 'hidden',
399 400 'dotted',
400 401 'dashed',
401 402 'solid',
402 403 'double',
403 404 'groove',
404 405 'ridge',
405 406 'inset',
406 407 'outset',
407 408 'initial',
408 409 'inherit', ''],
409 410 default_value='', sync=True)
410 411
411 412 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
412 413 'normal',
413 414 'italic',
414 415 'oblique',
415 416 'initial',
416 417 'inherit', ''],
417 418 default_value='', sync=True)
418 419 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
419 420 'normal',
420 421 'bold',
421 422 'bolder',
422 423 'lighter',
423 424 'initial',
424 425 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
425 426 default_value='', sync=True)
426 427 font_size = CUnicode(sync=True)
427 428 font_family = Unicode(sync=True)
429
430 def __init__(self, *pargs, **kwargs):
431 super(DOMWidget, self).__init__(*pargs, **kwargs)
432
433 def _validate_border(name, old, new):
434 if new is not None and new != '':
435 if name != 'border_width' and not self.border_width:
436 self.border_width = 1
437 if name != 'border_style' and self.border_style == '':
438 self.border_style = 'solid'
439 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,178 +1,179 b''
1 1 """Float class.
2 2
3 3 Represents an unbounded float using a widget.
4 4 """
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from .widget import DOMWidget
17 17 from IPython.utils.traitlets import Unicode, CFloat, Bool, CaselessStrEnum, Tuple
18 18 from IPython.utils.warn import DeprecatedClass
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Classes
22 22 #-----------------------------------------------------------------------------
23 23 class _Float(DOMWidget):
24 24 value = CFloat(0.0, help="Float value", sync=True)
25 25 disabled = Bool(False, help="Enable or disable user changes", sync=True)
26 26 description = Unicode(help="Description of the value this widget represents", sync=True)
27 27
28 28
29 29 class _BoundedFloat(_Float):
30 30 max = CFloat(100.0, help="Max value", sync=True)
31 31 min = CFloat(0.0, help="Min value", sync=True)
32 32 step = CFloat(0.1, help="Minimum step that the value can take (ignored by some views)", sync=True)
33 33
34 34 def __init__(self, *pargs, **kwargs):
35 35 """Constructor"""
36 36 DOMWidget.__init__(self, *pargs, **kwargs)
37 37 self._validate('value', None, self.value)
38 38 self.on_trait_change(self._validate, ['value', 'min', 'max'])
39 39
40 40 def _validate(self, name, old, new):
41 41 """Validate value, max, min."""
42 42 if self.min > new or new > self.max:
43 43 self.value = min(max(new, self.min), self.max)
44 44
45 45
46 46 class FloatText(_Float):
47 47 _view_name = Unicode('FloatTextView', sync=True)
48 48
49 49
50 50 class BoundedFloatText(_BoundedFloat):
51 51 _view_name = Unicode('FloatTextView', sync=True)
52 52
53 53
54 54 class FloatSlider(_BoundedFloat):
55 55 _view_name = Unicode('FloatSliderView', sync=True)
56 56 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
57 57 default_value='horizontal',
58 58 help="Vertical or horizontal.", allow_none=False, sync=True)
59 59 _range = Bool(False, help="Display a range selector", sync=True)
60 60 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
61 61 slider_color = Unicode(sync=True)
62 62
63 63
64 64 class FloatProgress(_BoundedFloat):
65 65 _view_name = Unicode('ProgressView', sync=True)
66 66
67 67 bar_style = CaselessStrEnum(
68 68 values=['success', 'info', 'warning', 'danger', ''],
69 69 default_value='', allow_none=True, sync=True, help="""Use a
70 70 predefined styling for the progess bar.""")
71 71
72 72 class _FloatRange(_Float):
73 73 value = Tuple(CFloat, CFloat, default_value=(0.0, 1.0), help="Tuple of (lower, upper) bounds", sync=True)
74 74 lower = CFloat(0.0, help="Lower bound", sync=False)
75 75 upper = CFloat(1.0, help="Upper bound", sync=False)
76 76
77 77 def __init__(self, *pargs, **kwargs):
78 78 value_given = 'value' in kwargs
79 79 lower_given = 'lower' in kwargs
80 80 upper_given = 'upper' in kwargs
81 81 if value_given and (lower_given or upper_given):
82 82 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
83 83 if lower_given != upper_given:
84 84 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
85 85
86 86 DOMWidget.__init__(self, *pargs, **kwargs)
87 87
88 88 # ensure the traits match, preferring whichever (if any) was given in kwargs
89 89 if value_given:
90 90 self.lower, self.upper = self.value
91 91 else:
92 92 self.value = (self.lower, self.upper)
93 93
94 94 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
95 95
96 96 def _validate(self, name, old, new):
97 97 if name == 'value':
98 98 self.lower, self.upper = min(new), max(new)
99 99 elif name == 'lower':
100 100 self.value = (new, self.value[1])
101 101 elif name == 'upper':
102 102 self.value = (self.value[0], new)
103 103
104 104 class _BoundedFloatRange(_FloatRange):
105 105 step = CFloat(1.0, help="Minimum step that the value can take (ignored by some views)", sync=True)
106 106 max = CFloat(100.0, help="Max value", sync=True)
107 107 min = CFloat(0.0, help="Min value", sync=True)
108 108
109 109 def __init__(self, *pargs, **kwargs):
110 110 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
111 111 _FloatRange.__init__(self, *pargs, **kwargs)
112 112
113 113 # ensure a minimal amount of sanity
114 114 if self.min > self.max:
115 115 raise ValueError("min must be <= max")
116 116
117 117 if any_value_given:
118 118 # if a value was given, clamp it within (min, max)
119 119 self._validate("value", None, self.value)
120 120 else:
121 121 # otherwise, set it to 25-75% to avoid the handles overlapping
122 122 self.value = (0.75*self.min + 0.25*self.max,
123 123 0.25*self.min + 0.75*self.max)
124 124 # callback already set for 'value', 'lower', 'upper'
125 125 self.on_trait_change(self._validate, ['min', 'max'])
126 126
127 127
128 128 def _validate(self, name, old, new):
129 129 if name == "min":
130 130 if new > self.max:
131 131 raise ValueError("setting min > max")
132 132 self.min = new
133 133 elif name == "max":
134 134 if new < self.min:
135 135 raise ValueError("setting max < min")
136 136 self.max = new
137 137
138 138 low, high = self.value
139 139 if name == "value":
140 140 low, high = min(new), max(new)
141 141 elif name == "upper":
142 142 if new < self.lower:
143 143 raise ValueError("setting upper < lower")
144 144 high = new
145 145 elif name == "lower":
146 146 if new > self.upper:
147 147 raise ValueError("setting lower > upper")
148 148 low = new
149 149
150 150 low = max(self.min, min(low, self.max))
151 151 high = min(self.max, max(high, self.min))
152 152
153 153 # determine the order in which we should update the
154 154 # lower, upper traits to avoid a temporary inverted overlap
155 155 lower_first = high < self.lower
156 156
157 157 self.value = (low, high)
158 158 if lower_first:
159 159 self.lower = low
160 160 self.upper = high
161 161 else:
162 162 self.upper = high
163 163 self.lower = low
164 164
165 165
166 166 class FloatRangeSlider(_BoundedFloatRange):
167 167 _view_name = Unicode('FloatSliderView', sync=True)
168 168 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
169 169 default_value='horizontal', allow_none=False,
170 170 help="Vertical or horizontal.", sync=True)
171 171 _range = Bool(True, help="Display a range selector", sync=True)
172 172 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
173 slider_color = Unicode(sync=True)
173 174
174 175 # Remove in IPython 4.0
175 176 FloatTextWidget = DeprecatedClass(FloatText, 'FloatTextWidget')
176 177 BoundedFloatTextWidget = DeprecatedClass(BoundedFloatText, 'BoundedFloatTextWidget')
177 178 FloatSliderWidget = DeprecatedClass(FloatSlider, 'FloatSliderWidget')
178 179 FloatProgressWidget = DeprecatedClass(FloatProgress, 'FloatProgressWidget')
@@ -1,182 +1,183 b''
1 1 """Int class.
2 2
3 3 Represents an unbounded int using a widget.
4 4 """
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from .widget import DOMWidget
17 17 from IPython.utils.traitlets import Unicode, CInt, Bool, CaselessStrEnum, Tuple
18 18 from IPython.utils.warn import DeprecatedClass
19 19
20 20 #-----------------------------------------------------------------------------
21 21 # Classes
22 22 #-----------------------------------------------------------------------------
23 23 class _Int(DOMWidget):
24 24 """Base class used to create widgets that represent an int."""
25 25 value = CInt(0, help="Int value", sync=True)
26 26 disabled = Bool(False, help="Enable or disable user changes", sync=True)
27 27 description = Unicode(help="Description of the value this widget represents", sync=True)
28 28
29 29
30 30 class _BoundedInt(_Int):
31 31 """Base class used to create widgets that represent a int that is bounded
32 32 by a minium and maximum."""
33 33 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
34 34 max = CInt(100, help="Max value", sync=True)
35 35 min = CInt(0, help="Min value", sync=True)
36 36
37 37 def __init__(self, *pargs, **kwargs):
38 38 """Constructor"""
39 39 DOMWidget.__init__(self, *pargs, **kwargs)
40 40 self.on_trait_change(self._validate, ['value', 'min', 'max'])
41 41
42 42 def _validate(self, name, old, new):
43 43 """Validate value, max, min."""
44 44 if self.min > new or new > self.max:
45 45 self.value = min(max(new, self.min), self.max)
46 46
47 47
48 48 class IntText(_Int):
49 49 """Textbox widget that represents a int."""
50 50 _view_name = Unicode('IntTextView', sync=True)
51 51
52 52
53 53 class BoundedIntText(_BoundedInt):
54 54 """Textbox widget that represents a int bounded by a minimum and maximum value."""
55 55 _view_name = Unicode('IntTextView', sync=True)
56 56
57 57
58 58 class IntSlider(_BoundedInt):
59 59 """Slider widget that represents a int bounded by a minimum and maximum value."""
60 60 _view_name = Unicode('IntSliderView', sync=True)
61 61 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
62 62 default_value='horizontal', allow_none=False,
63 63 help="Vertical or horizontal.", sync=True)
64 64 _range = Bool(False, help="Display a range selector", sync=True)
65 65 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
66 66 slider_color = Unicode(sync=True)
67 67
68 68
69 69 class IntProgress(_BoundedInt):
70 70 """Progress bar that represents a int bounded by a minimum and maximum value."""
71 71 _view_name = Unicode('ProgressView', sync=True)
72 72
73 73 bar_style = CaselessStrEnum(
74 74 values=['success', 'info', 'warning', 'danger', ''],
75 75 default_value='', allow_none=True, sync=True, help="""Use a
76 76 predefined styling for the progess bar.""")
77 77
78 78 class _IntRange(_Int):
79 79 value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
80 80 lower = CInt(0, help="Lower bound", sync=False)
81 81 upper = CInt(1, help="Upper bound", sync=False)
82 82
83 83 def __init__(self, *pargs, **kwargs):
84 84 value_given = 'value' in kwargs
85 85 lower_given = 'lower' in kwargs
86 86 upper_given = 'upper' in kwargs
87 87 if value_given and (lower_given or upper_given):
88 88 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
89 89 if lower_given != upper_given:
90 90 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
91 91
92 92 DOMWidget.__init__(self, *pargs, **kwargs)
93 93
94 94 # ensure the traits match, preferring whichever (if any) was given in kwargs
95 95 if value_given:
96 96 self.lower, self.upper = self.value
97 97 else:
98 98 self.value = (self.lower, self.upper)
99 99
100 100 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
101 101
102 102 def _validate(self, name, old, new):
103 103 if name == 'value':
104 104 self.lower, self.upper = min(new), max(new)
105 105 elif name == 'lower':
106 106 self.value = (new, self.value[1])
107 107 elif name == 'upper':
108 108 self.value = (self.value[0], new)
109 109
110 110 class _BoundedIntRange(_IntRange):
111 111 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
112 112 max = CInt(100, help="Max value", sync=True)
113 113 min = CInt(0, help="Min value", sync=True)
114 114
115 115 def __init__(self, *pargs, **kwargs):
116 116 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
117 117 _IntRange.__init__(self, *pargs, **kwargs)
118 118
119 119 # ensure a minimal amount of sanity
120 120 if self.min > self.max:
121 121 raise ValueError("min must be <= max")
122 122
123 123 if any_value_given:
124 124 # if a value was given, clamp it within (min, max)
125 125 self._validate("value", None, self.value)
126 126 else:
127 127 # otherwise, set it to 25-75% to avoid the handles overlapping
128 128 self.value = (0.75*self.min + 0.25*self.max,
129 129 0.25*self.min + 0.75*self.max)
130 130 # callback already set for 'value', 'lower', 'upper'
131 131 self.on_trait_change(self._validate, ['min', 'max'])
132 132
133 133 def _validate(self, name, old, new):
134 134 if name == "min":
135 135 if new > self.max:
136 136 raise ValueError("setting min > max")
137 137 self.min = new
138 138 elif name == "max":
139 139 if new < self.min:
140 140 raise ValueError("setting max < min")
141 141 self.max = new
142 142
143 143 low, high = self.value
144 144 if name == "value":
145 145 low, high = min(new), max(new)
146 146 elif name == "upper":
147 147 if new < self.lower:
148 148 raise ValueError("setting upper < lower")
149 149 high = new
150 150 elif name == "lower":
151 151 if new > self.upper:
152 152 raise ValueError("setting lower > upper")
153 153 low = new
154 154
155 155 low = max(self.min, min(low, self.max))
156 156 high = min(self.max, max(high, self.min))
157 157
158 158 # determine the order in which we should update the
159 159 # lower, upper traits to avoid a temporary inverted overlap
160 160 lower_first = high < self.lower
161 161
162 162 self.value = (low, high)
163 163 if lower_first:
164 164 self.lower = low
165 165 self.upper = high
166 166 else:
167 167 self.upper = high
168 168 self.lower = low
169 169
170 170 class IntRangeSlider(_BoundedIntRange):
171 171 _view_name = Unicode('IntSliderView', sync=True)
172 172 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
173 173 default_value='horizontal', allow_none=False,
174 174 help="Vertical or horizontal.", sync=True)
175 175 _range = Bool(True, help="Display a range selector", sync=True)
176 176 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
177 slider_color = Unicode(sync=True)
177 178
178 179 # Remove in IPython 4.0
179 180 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
180 181 BoundedIntTextWidget = DeprecatedClass(BoundedIntText, 'BoundedIntTextWidget')
181 182 IntSliderWidget = DeprecatedClass(IntSlider, 'IntSliderWidget')
182 183 IntProgressWidget = DeprecatedClass(IntProgress, 'IntProgressWidget')
General Comments 0
You need to be logged in to leave comments. Login now