##// END OF EJS Templates
Initial crack at using specific traits for styling.
Jonathan Frederic -
Show More
@@ -1,477 +1,504
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.after_displayed(function() {
413 413 this.update_visible(this.model, this.model.get("visible"));
414 414 this.update_css(this.model, this.model.get("_css"));
415 415 }, this);
416 416 this.model.on('change:visible', this.update_visible, this);
417 417 this.model.on('change:_css', this.update_css, this);
418 418 this.model.on('change:_dom_classes', function(model, new_classes) {
419 419 var old_classes = model.previous('children');
420 420 this.update_classes(old_classes, new_classes);
421 421 }, this);
422 this.model.on('change:fore_color', function (model, value) {
423 this.update_attr('color', value); }, this);
424 this.model.on('change:back_color', function (model, value) {
425 this.update_attr('background', value); }, this);
426 this.model.on('change:width', function (model, value) {
427 this.update_attr('width', value); }, this);
428 this.model.on('change:height', function (model, value) {
429 this.update_attr('height', value); }, this);
430 this.model.on('change:border_color', function (model, value) {
431 this.update_attr('border-color', value); }, this);
432 this.model.on('change:border_width', function (model, value) {
433 this.update_attr('border-width', value); }, this);
434 this.model.on('change:border_style', function (model, value) {
435 this.update_attr('border-style', value); }, this);
436 this.model.on('change:font_style', function (model, value) {
437 this.update_attr('font-style', value); }, this);
438 this.model.on('change:font_weight', function (model, value) {
439 this.update_attr('font-weight', value); }, this);
440 this.model.on('change:font_size', function (model, value) {
441 this.update_attr('font-size', value); }, this);
442 this.model.on('change:font_family', function (model, value) {
443 this.update_attr('font-family', value); }, this);
422 444 this.update_classes([], this.model.get('_dom_classes'));
423 445 },
424 446
447 update_attr: function(name, value) {
448 // Set a css attr of the widget view.
449 this.$el.css(name, value);
450 },
451
425 452 update_visible: function(model, value) {
426 453 // Update visibility
427 454 this.$el.toggle(value);
428 455 },
429 456
430 457 update_css: function (model, css) {
431 458 // Update the css styling of this view.
432 459 var e = this.$el;
433 460 if (css === undefined) {return;}
434 461 for (var i = 0; i < css.length; i++) {
435 462 // Apply the css traits to all elements that match the selector.
436 463 var selector = css[i][0];
437 464 var elements = this._get_selector_element(selector);
438 465 if (elements.length > 0) {
439 466 var trait_key = css[i][1];
440 467 var trait_value = css[i][2];
441 468 elements.css(trait_key ,trait_value);
442 469 }
443 470 }
444 471 },
445 472
446 473 update_classes: function (old_classes, new_classes) {
447 474 // Update the DOM classes applied to the topmost element.
448 475 this.do_diff(old_classes, new_classes, function(removed) {
449 476 this.$el.removeClass(removed);
450 477 }, function(added) {
451 478 this.$el.addClass(added);
452 479 });
453 480 },
454 481
455 482 _get_selector_element: function (selector) {
456 483 // Get the elements via the css selector.
457 484 var elements;
458 485 if (!selector) {
459 486 elements = this.$el;
460 487 } else {
461 488 elements = this.$el.find(selector).addBack(selector);
462 489 }
463 490 return elements;
464 491 },
465 492 });
466 493
467 494 var widget = {
468 495 'WidgetModel': WidgetModel,
469 496 'WidgetView': WidgetView,
470 497 'DOMWidgetView': DOMWidgetView,
471 498 };
472 499
473 500 // For backwards compatability.
474 501 $.extend(IPython, widget);
475 502
476 503 return widget;
477 504 });
@@ -1,307 +1,312
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 },
18 18
19 update_attr: function(name, value) {
20 // Set a css attr of the widget view.
21 this.$box.css(name, value);
22 },
23
19 24 render: function(){
20 25 // Called when view is rendered.
21 26 this.$box = this.$el;
22 27 this.$box.addClass('widget-box');
23 28 this.update_children([], this.model.get('children'));
24 29 },
25 30
26 31 update_children: function(old_list, new_list) {
27 32 // Called when the children list changes.
28 33 this.do_diff(old_list, new_list,
29 34 $.proxy(this.remove_child_model, this),
30 35 $.proxy(this.add_child_model, this));
31 36 },
32 37
33 38 remove_child_model: function(model) {
34 39 // Called when a model is removed from the children list.
35 40 this.pop_child_view(model).remove();
36 41 },
37 42
38 43 add_child_model: function(model) {
39 44 // Called when a model is added to the children list.
40 45 var view = this.create_child_view(model);
41 46 this.$box.append(view.$el);
42 47
43 48 // Trigger the displayed event of the child view.
44 49 this.after_displayed(function() {
45 50 view.trigger('displayed');
46 51 });
47 52 },
48 53 });
49 54
50 55
51 56 var FlexBoxView = BoxView.extend({
52 57 render: function(){
53 58 FlexBoxView.__super__.render.apply(this);
54 59 this.model.on('change:orientation', this.update_orientation, this);
55 60 this.model.on('change:flex', this._flex_changed, this);
56 61 this.model.on('change:pack', this._pack_changed, this);
57 62 this.model.on('change:align', this._align_changed, this);
58 63 this._flex_changed();
59 64 this._pack_changed();
60 65 this._align_changed();
61 66 this.update_orientation();
62 67 },
63 68
64 69 update_orientation: function(){
65 70 var orientation = this.model.get("orientation");
66 71 if (orientation == "vertical") {
67 72 this.$box.removeClass("hbox").addClass("vbox");
68 73 } else {
69 74 this.$box.removeClass("vbox").addClass("hbox");
70 75 }
71 76 },
72 77
73 78 _flex_changed: function(){
74 79 if (this.model.previous('flex')) {
75 80 this.$box.removeClass('box-flex' + this.model.previous('flex'));
76 81 }
77 82 this.$box.addClass('box-flex' + this.model.get('flex'));
78 83 },
79 84
80 85 _pack_changed: function(){
81 86 if (this.model.previous('pack')) {
82 87 this.$box.removeClass(this.model.previous('pack'));
83 88 }
84 89 this.$box.addClass(this.model.get('pack'));
85 90 },
86 91
87 92 _align_changed: function(){
88 93 if (this.model.previous('align')) {
89 94 this.$box.removeClass('align-' + this.model.previous('align'));
90 95 }
91 96 this.$box.addClass('align-' + this.model.get('align'));
92 97 },
93 98 });
94 99
95 100 var PopupView = BoxView.extend({
96 101
97 102 render: function(){
98 103 // Called when view is rendered.
99 104 var that = this;
100 105
101 106 this.$el.on("remove", function(){
102 107 that.$backdrop.remove();
103 108 });
104 109 this.$backdrop = $('<div />')
105 110 .appendTo($('#notebook-container'))
106 111 .addClass('modal-dialog')
107 112 .css('position', 'absolute')
108 113 .css('left', '0px')
109 114 .css('top', '0px');
110 115 this.$window = $('<div />')
111 116 .appendTo(this.$backdrop)
112 117 .addClass('modal-content widget-modal')
113 118 .mousedown(function(){
114 119 that.bring_to_front();
115 120 });
116 121
117 122 // Set the elements array since the this.$window element is not child
118 123 // of this.$el and the parent widget manager or other widgets may
119 124 // need to know about all of the top-level widgets. The IPython
120 125 // widget manager uses this to register the elements with the
121 126 // keyboard manager.
122 127 this.additional_elements = [this.$window];
123 128
124 129 this.$title_bar = $('<div />')
125 130 .addClass('popover-title')
126 131 .appendTo(this.$window)
127 132 .mousedown(function(){
128 133 that.bring_to_front();
129 134 });
130 135 this.$close = $('<button />')
131 136 .addClass('close fa fa-remove')
132 137 .css('margin-left', '5px')
133 138 .appendTo(this.$title_bar)
134 139 .click(function(){
135 140 that.hide();
136 141 event.stopPropagation();
137 142 });
138 143 this.$minimize = $('<button />')
139 144 .addClass('close fa fa-arrow-down')
140 145 .appendTo(this.$title_bar)
141 146 .click(function(){
142 147 that.popped_out = !that.popped_out;
143 148 if (!that.popped_out) {
144 149 that.$minimize
145 150 .removeClass('fa fa-arrow-down')
146 151 .addClass('fa fa-arrow-up');
147 152
148 153 that.$window
149 154 .draggable('destroy')
150 155 .resizable('destroy')
151 156 .removeClass('widget-modal modal-content')
152 157 .addClass('docked-widget-modal')
153 158 .detach()
154 159 .insertBefore(that.$show_button);
155 160 that.$show_button.hide();
156 161 that.$close.hide();
157 162 } else {
158 163 that.$minimize
159 164 .addClass('fa fa-arrow-down')
160 165 .removeClass('fa fa-arrow-up');
161 166
162 167 that.$window
163 168 .removeClass('docked-widget-modal')
164 169 .addClass('widget-modal modal-content')
165 170 .detach()
166 171 .appendTo(that.$backdrop)
167 172 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
168 173 .resizable()
169 174 .children('.ui-resizable-handle').show();
170 175 that.show();
171 176 that.$show_button.show();
172 177 that.$close.show();
173 178 }
174 179 event.stopPropagation();
175 180 });
176 181 this.$title = $('<div />')
177 182 .addClass('widget-modal-title')
178 183 .html("&nbsp;")
179 184 .appendTo(this.$title_bar);
180 185 this.$box = $('<div />')
181 186 .addClass('modal-body')
182 187 .addClass('widget-modal-body')
183 188 .addClass('widget-box')
184 189 .addClass('vbox')
185 190 .appendTo(this.$window);
186 191
187 192 this.$show_button = $('<button />')
188 193 .html("&nbsp;")
189 194 .addClass('btn btn-info widget-modal-show')
190 195 .appendTo(this.$el)
191 196 .click(function(){
192 197 that.show();
193 198 });
194 199
195 200 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
196 201 this.$window.resizable();
197 202 this.$window.on('resize', function(){
198 203 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
199 204 });
200 205
201 206 this._shown_once = false;
202 207 this.popped_out = true;
203 208
204 209 this.update_children([], this.model.get('children'));
205 210 this.model.on('change:children', function(model, value) {
206 211 this.update_children(model.previous('children'), value);
207 212 }, this);
208 213 },
209 214
210 215 hide: function() {
211 216 // Called when the modal hide button is clicked.
212 217 this.$window.hide();
213 218 this.$show_button.removeClass('btn-info');
214 219 },
215 220
216 221 show: function() {
217 222 // Called when the modal show button is clicked.
218 223 this.$show_button.addClass('btn-info');
219 224 this.$window.show();
220 225 if (this.popped_out) {
221 226 this.$window.css("positon", "absolute");
222 227 this.$window.css("top", "0px");
223 228 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
224 229 $(window).scrollLeft()) + "px");
225 230 this.bring_to_front();
226 231 }
227 232 },
228 233
229 234 bring_to_front: function() {
230 235 // Make the modal top-most, z-ordered about the other modals.
231 236 var $widget_modals = $(".widget-modal");
232 237 var max_zindex = 0;
233 238 $widget_modals.each(function (index, el){
234 239 var zindex = parseInt($(el).css('z-index'));
235 240 if (!isNaN(zindex)) {
236 241 max_zindex = Math.max(max_zindex, zindex);
237 242 }
238 243 });
239 244
240 245 // Start z-index of widget modals at 2000
241 246 max_zindex = Math.max(max_zindex, 2000);
242 247
243 248 $widget_modals.each(function (index, el){
244 249 $el = $(el);
245 250 if (max_zindex == parseInt($el.css('z-index'))) {
246 251 $el.css('z-index', max_zindex - 1);
247 252 }
248 253 });
249 254 this.$window.css('z-index', max_zindex);
250 255 },
251 256
252 257 update: function(){
253 258 // Update the contents of this view
254 259 //
255 260 // Called when the model is changed. The model may have been
256 261 // changed by another view or by a state update from the back-end.
257 262 var description = this.model.get('description');
258 263 if (description.trim().length === 0) {
259 264 this.$title.html("&nbsp;"); // Preserve title height
260 265 } else {
261 266 this.$title.text(description);
262 267 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
263 268 }
264 269
265 270 var button_text = this.model.get('button_text');
266 271 if (button_text.trim().length === 0) {
267 272 this.$show_button.html("&nbsp;"); // Preserve button height
268 273 } else {
269 274 this.$show_button.text(button_text);
270 275 }
271 276
272 277 if (!this._shown_once) {
273 278 this._shown_once = true;
274 279 this.show();
275 280 }
276 281
277 282 return PopupView.__super__.update.apply(this);
278 283 },
279 284
280 285 _get_selector_element: function(selector) {
281 286 // Get an element view a 'special' jquery selector. (see widget.js)
282 287 //
283 288 // Since the modal actually isn't within the $el in the DOM, we need to extend
284 289 // the selector logic to allow the user to set css on the modal if need be.
285 290 // The convention used is:
286 291 // "modal" - select the modal div
287 292 // "modal [selector]" - select element(s) within the modal div.
288 293 // "[selector]" - select elements within $el
289 294 // "" - select the $el
290 295 if (selector.substring(0, 5) == 'modal') {
291 296 if (selector == 'modal') {
292 297 return this.$window;
293 298 } else {
294 299 return this.$window.find(selector.substring(6));
295 300 }
296 301 } else {
297 302 return PopupView.__super__._get_selector_element.apply(this, [selector]);
298 303 }
299 304 },
300 305 });
301 306
302 307 return {
303 308 'BoxView': BoxView,
304 309 'PopupView': PopupView,
305 310 'FlexBoxView': FlexBoxView,
306 311 };
307 312 });
@@ -1,383 +1,425
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 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple, Int, Set
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List,
22 CaselessStrEnum, Tuple, CTuple, CUnicode, Int, Set
22 23 from IPython.utils.py3compat import string_types
23 24
24 25 #-----------------------------------------------------------------------------
25 26 # Classes
26 27 #-----------------------------------------------------------------------------
27 28 class CallbackDispatcher(LoggingConfigurable):
28 29 """A structure for registering and running callbacks"""
29 30 callbacks = List()
30 31
31 32 def __call__(self, *args, **kwargs):
32 33 """Call all of the registered callbacks."""
33 34 value = None
34 35 for callback in self.callbacks:
35 36 try:
36 37 local_value = callback(*args, **kwargs)
37 38 except Exception as e:
38 39 ip = get_ipython()
39 40 if ip is None:
40 41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
41 42 else:
42 43 ip.showtraceback()
43 44 else:
44 45 value = local_value if local_value is not None else value
45 46 return value
46 47
47 48 def register_callback(self, callback, remove=False):
48 49 """(Un)Register a callback
49 50
50 51 Parameters
51 52 ----------
52 53 callback: method handle
53 54 Method to be registered or unregistered.
54 55 remove=False: bool
55 56 Whether to unregister the callback."""
56 57
57 58 # (Un)Register the callback.
58 59 if remove and callback in self.callbacks:
59 60 self.callbacks.remove(callback)
60 61 elif not remove and callback not in self.callbacks:
61 62 self.callbacks.append(callback)
62 63
63 64 def _show_traceback(method):
64 65 """decorator for showing tracebacks in IPython"""
65 66 def m(self, *args, **kwargs):
66 67 try:
67 68 return(method(self, *args, **kwargs))
68 69 except Exception as e:
69 70 ip = get_ipython()
70 71 if ip is None:
71 72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
72 73 else:
73 74 ip.showtraceback()
74 75 return m
75 76
76 77 class Widget(LoggingConfigurable):
77 78 #-------------------------------------------------------------------------
78 79 # Class attributes
79 80 #-------------------------------------------------------------------------
80 81 _widget_construction_callback = None
81 82 widgets = {}
82 83
83 84 @staticmethod
84 85 def on_widget_constructed(callback):
85 86 """Registers a callback to be called when a widget is constructed.
86 87
87 88 The callback must have the following signature:
88 89 callback(widget)"""
89 90 Widget._widget_construction_callback = callback
90 91
91 92 @staticmethod
92 93 def _call_widget_constructed(widget):
93 94 """Static method, called when a widget is constructed."""
94 95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
95 96 Widget._widget_construction_callback(widget)
96 97
97 98 #-------------------------------------------------------------------------
98 99 # Traits
99 100 #-------------------------------------------------------------------------
100 101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
101 102 registered in the front-end to create and sync this widget with.""")
102 103 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
103 104 to use to represent the widget.""", sync=True)
104 105 comm = Instance('IPython.kernel.comm.Comm')
105 106
106 107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
107 108 front-end can send before receiving an idle msg from the back-end.""")
108 109
109 110 keys = List()
110 111 def _keys_default(self):
111 112 return [name for name in self.traits(sync=True)]
112 113
113 114 _property_lock = Tuple((None, None))
114 115 _send_state_lock = Int(0)
115 116 _states_to_send = Set(allow_none=False)
116 117 _display_callbacks = Instance(CallbackDispatcher, ())
117 118 _msg_callbacks = Instance(CallbackDispatcher, ())
118 119
119 120 #-------------------------------------------------------------------------
120 121 # (Con/de)structor
121 122 #-------------------------------------------------------------------------
122 123 def __init__(self, **kwargs):
123 124 """Public constructor"""
124 125 self._model_id = kwargs.pop('model_id', None)
125 126 super(Widget, self).__init__(**kwargs)
126 127
127 128 self.on_trait_change(self._handle_property_changed, self.keys)
128 129 Widget._call_widget_constructed(self)
129 130 self.open()
130 131
131 132 def __del__(self):
132 133 """Object disposal"""
133 134 self.close()
134 135
135 136 #-------------------------------------------------------------------------
136 137 # Properties
137 138 #-------------------------------------------------------------------------
138 139
139 140 def open(self):
140 141 """Open a comm to the frontend if one isn't already open."""
141 142 if self.comm is None:
142 143 if self._model_id is None:
143 144 self.comm = Comm(target_name=self._model_name)
144 145 self._model_id = self.model_id
145 146 else:
146 147 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
147 148 self.comm.on_msg(self._handle_msg)
148 149 Widget.widgets[self.model_id] = self
149 150
150 151 # first update
151 152 self.send_state()
152 153
153 154 @property
154 155 def model_id(self):
155 156 """Gets the model id of this widget.
156 157
157 158 If a Comm doesn't exist yet, a Comm will be created automagically."""
158 159 return self.comm.comm_id
159 160
160 161 #-------------------------------------------------------------------------
161 162 # Methods
162 163 #-------------------------------------------------------------------------
163 164
164 165 def close(self):
165 166 """Close method.
166 167
167 168 Closes the underlying comm.
168 169 When the comm is closed, all of the widget views are automatically
169 170 removed from the front-end."""
170 171 if self.comm is not None:
171 172 Widget.widgets.pop(self.model_id, None)
172 173 self.comm.close()
173 174 self.comm = None
174 175
175 176 def send_state(self, key=None):
176 177 """Sends the widget state, or a piece of it, to the front-end.
177 178
178 179 Parameters
179 180 ----------
180 181 key : unicode, or iterable (optional)
181 182 A single property's name or iterable of property names to sync with the front-end.
182 183 """
183 184 self._send({
184 185 "method" : "update",
185 186 "state" : self.get_state(key=key)
186 187 })
187 188
188 189 def get_state(self, key=None):
189 190 """Gets the widget state, or a piece of it.
190 191
191 192 Parameters
192 193 ----------
193 194 key : unicode or iterable (optional)
194 195 A single property's name or iterable of property names to get.
195 196 """
196 197 if key is None:
197 198 keys = self.keys
198 199 elif isinstance(key, string_types):
199 200 keys = [key]
200 201 elif isinstance(key, collections.Iterable):
201 202 keys = key
202 203 else:
203 204 raise ValueError("key must be a string, an iterable of keys, or None")
204 205 state = {}
205 206 for k in keys:
206 207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
207 208 value = getattr(self, k)
208 209 state[k] = f(value)
209 210 return state
210 211
211 212 def send(self, content):
212 213 """Sends a custom msg to the widget model in the front-end.
213 214
214 215 Parameters
215 216 ----------
216 217 content : dict
217 218 Content of the message to send.
218 219 """
219 220 self._send({"method": "custom", "content": content})
220 221
221 222 def on_msg(self, callback, remove=False):
222 223 """(Un)Register a custom msg receive callback.
223 224
224 225 Parameters
225 226 ----------
226 227 callback: callable
227 228 callback will be passed two arguments when a message arrives::
228 229
229 230 callback(widget, content)
230 231
231 232 remove: bool
232 233 True if the callback should be unregistered."""
233 234 self._msg_callbacks.register_callback(callback, remove=remove)
234 235
235 236 def on_displayed(self, callback, remove=False):
236 237 """(Un)Register a widget displayed callback.
237 238
238 239 Parameters
239 240 ----------
240 241 callback: method handler
241 242 Must have a signature of::
242 243
243 244 callback(widget, **kwargs)
244 245
245 246 kwargs from display are passed through without modification.
246 247 remove: bool
247 248 True if the callback should be unregistered."""
248 249 self._display_callbacks.register_callback(callback, remove=remove)
249 250
250 251 #-------------------------------------------------------------------------
251 252 # Support methods
252 253 #-------------------------------------------------------------------------
253 254 @contextmanager
254 255 def _lock_property(self, key, value):
255 256 """Lock a property-value pair.
256 257
257 258 The value should be the JSON state of the property.
258 259
259 260 NOTE: This, in addition to the single lock for all state changes, is
260 261 flawed. In the future we may want to look into buffering state changes
261 262 back to the front-end."""
262 263 self._property_lock = (key, value)
263 264 try:
264 265 yield
265 266 finally:
266 267 self._property_lock = (None, None)
267 268
268 269 @contextmanager
269 270 def hold_sync(self):
270 271 """Hold syncing any state until the context manager is released"""
271 272 # We increment a value so that this can be nested. Syncing will happen when
272 273 # all levels have been released.
273 274 self._send_state_lock += 1
274 275 try:
275 276 yield
276 277 finally:
277 278 self._send_state_lock -=1
278 279 if self._send_state_lock == 0:
279 280 self.send_state(self._states_to_send)
280 281 self._states_to_send.clear()
281 282
282 283 def _should_send_property(self, key, value):
283 284 """Check the property lock (property_lock)"""
284 285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
285 286 if (key == self._property_lock[0]
286 287 and to_json(value) == self._property_lock[1]):
287 288 return False
288 289 elif self._send_state_lock > 0:
289 290 self._states_to_send.add(key)
290 291 return False
291 292 else:
292 293 return True
293 294
294 295 # Event handlers
295 296 @_show_traceback
296 297 def _handle_msg(self, msg):
297 298 """Called when a msg is received from the front-end"""
298 299 data = msg['content']['data']
299 300 method = data['method']
300 301 if not method in ['backbone', 'custom']:
301 302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
302 303
303 304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
304 305 if method == 'backbone' and 'sync_data' in data:
305 306 sync_data = data['sync_data']
306 307 self._handle_receive_state(sync_data) # handles all methods
307 308
308 309 # Handle a custom msg from the front-end
309 310 elif method == 'custom':
310 311 if 'content' in data:
311 312 self._handle_custom_msg(data['content'])
312 313
313 314 def _handle_receive_state(self, sync_data):
314 315 """Called when a state is received from the front-end."""
315 316 for name in self.keys:
316 317 if name in sync_data:
317 318 json_value = sync_data[name]
318 319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
319 320 with self._lock_property(name, json_value):
320 321 setattr(self, name, from_json(json_value))
321 322
322 323 def _handle_custom_msg(self, content):
323 324 """Called when a custom msg is received."""
324 325 self._msg_callbacks(self, content)
325 326
326 327 def _handle_property_changed(self, name, old, new):
327 328 """Called when a property has been changed."""
328 329 # Make sure this isn't information that the front-end just sent us.
329 330 if self._should_send_property(name, new):
330 331 # Send new state to front-end
331 332 self.send_state(key=name)
332 333
333 334 def _handle_displayed(self, **kwargs):
334 335 """Called when a view has been displayed for this widget instance"""
335 336 self._display_callbacks(self, **kwargs)
336 337
337 338 def _trait_to_json(self, x):
338 339 """Convert a trait value to json
339 340
340 341 Traverse lists/tuples and dicts and serialize their values as well.
341 342 Replace any widgets with their model_id
342 343 """
343 344 if isinstance(x, dict):
344 345 return {k: self._trait_to_json(v) for k, v in x.items()}
345 346 elif isinstance(x, (list, tuple)):
346 347 return [self._trait_to_json(v) for v in x]
347 348 elif isinstance(x, Widget):
348 349 return "IPY_MODEL_" + x.model_id
349 350 else:
350 351 return x # Value must be JSON-able
351 352
352 353 def _trait_from_json(self, x):
353 354 """Convert json values to objects
354 355
355 356 Replace any strings representing valid model id values to Widget references.
356 357 """
357 358 if isinstance(x, dict):
358 359 return {k: self._trait_from_json(v) for k, v in x.items()}
359 360 elif isinstance(x, (list, tuple)):
360 361 return [self._trait_from_json(v) for v in x]
361 362 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
362 363 # we want to support having child widgets at any level in a hierarchy
363 364 # trusting that a widget UUID will not appear out in the wild
364 365 return Widget.widgets[x]
365 366 else:
366 367 return x
367 368
368 369 def _ipython_display_(self, **kwargs):
369 370 """Called when `IPython.display.display` is called on the widget."""
370 371 # Show view. By sending a display message, the comm is opened and the
371 372 # initial state is sent.
372 373 self._send({"method": "display"})
373 374 self._handle_displayed(**kwargs)
374 375
375 376 def _send(self, msg):
376 377 """Sends a message to the model in the front-end."""
377 378 self.comm.send(msg)
378 379
379 380
380 381 class DOMWidget(Widget):
381 382 visible = Bool(True, help="Whether the widget is visible.", sync=True)
382 383 _css = CTuple(sync=True, help="CSS property list: (selector, key, value)")
383 384 _dom_classes = CTuple(sync=True, help="DOM classes applied to widget.$el.")
385
386 width = CUnicode(sync=True)
387 height = CUnicode(sync=True)
388
389 fore_color = Unicode(sync=True)
390 back_color = Unicode(sync=True)
391 border_color = Unicode(sync=True)
392
393 border_width = CUnicode(sync=True)
394 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
395 'none',
396 'hidden',
397 'dotted',
398 'dashed',
399 'solid',
400 'double',
401 'groove',
402 'ridge',
403 'inset',
404 'outset',
405 'initial',
406 'inherit', ''],
407 default_value='', sync=True)
408
409 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
410 'normal',
411 'italic',
412 'oblique',
413 'initial',
414 'inherit', ''],
415 default_value='', sync=True)
416 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
417 'normal',
418 'bold',
419 'bolder',
420 'lighter',
421 'initial',
422 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
423 default_value='', sync=True)
424 font_size = CUnicode(sync=True)
425 font_family = Unicode(sync=True)
General Comments 0
You need to be logged in to leave comments. Login now