##// END OF EJS Templates
Persistence API,...
Jonathan Frederic -
Show More
@@ -1,573 +1,602 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3 /**
4 4 *
5 5 *
6 6 * @module codecell
7 7 * @namespace codecell
8 8 * @class CodeCell
9 9 */
10 10
11 11
12 12 define([
13 13 'base/js/namespace',
14 14 'jquery',
15 15 'base/js/utils',
16 16 'base/js/keyboard',
17 17 'notebook/js/cell',
18 18 'notebook/js/outputarea',
19 19 'notebook/js/completer',
20 20 'notebook/js/celltoolbar',
21 21 'codemirror/lib/codemirror',
22 22 'codemirror/mode/python/python',
23 23 'notebook/js/codemirror-ipython'
24 24 ], function(IPython, $, utils, keyboard, cell, outputarea, completer, celltoolbar, CodeMirror, cmpython, cmip) {
25 25 "use strict";
26 26
27 27 var Cell = cell.Cell;
28 28
29 29 /* local util for codemirror */
30 30 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;};
31 31
32 32 /**
33 33 *
34 34 * function to delete until previous non blanking space character
35 35 * or first multiple of 4 tabstop.
36 36 * @private
37 37 */
38 38 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
39 39 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
40 40 if (!posEq(from, to)) { cm.replaceRange("", from, to); return; }
41 41 var cur = cm.getCursor(), line = cm.getLine(cur.line);
42 42 var tabsize = cm.getOption('tabSize');
43 43 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
44 44 from = {ch:cur.ch-chToPrevTabStop,line:cur.line};
45 45 var select = cm.getRange(from,cur);
46 46 if( select.match(/^\ +$/) !== null){
47 47 cm.replaceRange("",from,cur);
48 48 } else {
49 49 cm.deleteH(-1,"char");
50 50 }
51 51 };
52 52
53 53 var keycodes = keyboard.keycodes;
54 54
55 55 var CodeCell = function (kernel, options) {
56 56 /**
57 57 * Constructor
58 58 *
59 59 * A Cell conceived to write code.
60 60 *
61 61 * Parameters:
62 62 * kernel: Kernel instance
63 63 * The kernel doesn't have to be set at creation time, in that case
64 64 * it will be null and set_kernel has to be called later.
65 65 * options: dictionary
66 66 * Dictionary of keyword arguments.
67 67 * events: $(Events) instance
68 68 * config: dictionary
69 69 * keyboard_manager: KeyboardManager instance
70 70 * notebook: Notebook instance
71 71 * tooltip: Tooltip instance
72 72 */
73 73 this.kernel = kernel || null;
74 74 this.notebook = options.notebook;
75 75 this.collapsed = false;
76 76 this.events = options.events;
77 77 this.tooltip = options.tooltip;
78 78 this.config = options.config;
79 79
80 80 // create all attributed in constructor function
81 81 // even if null for V8 VM optimisation
82 82 this.input_prompt_number = null;
83 83 this.celltoolbar = null;
84 84 this.output_area = null;
85 85 // Keep a stack of the 'active' output areas (where active means the
86 86 // output area that recieves output). When a user activates an output
87 87 // area, it gets pushed to the stack. Then, when the output area is
88 88 // deactivated, it's popped from the stack. When the stack is empty,
89 89 // the cell's output area is used.
90 90 this.active_output_areas = [];
91 91 var that = this;
92 92 Object.defineProperty(this, 'active_output_area', {
93 93 get: function() {
94 94 if (that.active_output_areas && that.active_output_areas.length > 0) {
95 95 return that.active_output_areas[that.active_output_areas.length-1];
96 96 } else {
97 97 return that.output_area;
98 98 }
99 99 },
100 100 });
101 101
102 102 this.last_msg_id = null;
103 103 this.completer = null;
104
104 this.widget_views = [];
105 105
106 106 var config = utils.mergeopt(CodeCell, this.config);
107 107 Cell.apply(this,[{
108 108 config: config,
109 109 keyboard_manager: options.keyboard_manager,
110 110 events: this.events}]);
111 111
112 112 // Attributes we want to override in this subclass.
113 113 this.cell_type = "code";
114 114 this.element.focusout(
115 115 function() { that.auto_highlight(); }
116 116 );
117 117 };
118 118
119 119 CodeCell.options_default = {
120 120 cm_config : {
121 121 extraKeys: {
122 122 "Tab" : "indentMore",
123 123 "Shift-Tab" : "indentLess",
124 124 "Backspace" : "delSpaceToPrevTabStop",
125 125 "Cmd-/" : "toggleComment",
126 126 "Ctrl-/" : "toggleComment"
127 127 },
128 128 mode: 'ipython',
129 129 theme: 'ipython',
130 130 matchBrackets: true
131 131 }
132 132 };
133 133
134 134 CodeCell.msg_cells = {};
135 135
136 136 CodeCell.prototype = Object.create(Cell.prototype);
137 137
138 138 /**
139 139 * @method push_output_area
140 140 */
141 141 CodeCell.prototype.push_output_area = function (output_area) {
142 142 this.active_output_areas.push(output_area);
143 143 };
144 144
145 145 /**
146 146 * @method pop_output_area
147 147 */
148 148 CodeCell.prototype.pop_output_area = function (output_area) {
149 149 var index = this.active_output_areas.lastIndexOf(output_area);
150 150 if (index > -1) {
151 151 this.active_output_areas.splice(index, 1);
152 152 }
153 153 };
154 154
155 155 /**
156 156 * @method auto_highlight
157 157 */
158 158 CodeCell.prototype.auto_highlight = function () {
159 159 this._auto_highlight(this.config.cell_magic_highlight);
160 160 };
161 161
162 162 /** @method create_element */
163 163 CodeCell.prototype.create_element = function () {
164 164 Cell.prototype.create_element.apply(this, arguments);
165 165
166 166 var cell = $('<div></div>').addClass('cell code_cell');
167 167 cell.attr('tabindex','2');
168 168
169 169 var input = $('<div></div>').addClass('input');
170 170 var prompt = $('<div/>').addClass('prompt input_prompt');
171 171 var inner_cell = $('<div/>').addClass('inner_cell');
172 172 this.celltoolbar = new celltoolbar.CellToolbar({
173 173 cell: this,
174 174 notebook: this.notebook});
175 175 inner_cell.append(this.celltoolbar.element);
176 176 var input_area = $('<div/>').addClass('input_area');
177 177 this.code_mirror = new CodeMirror(input_area.get(0), this.cm_config);
178 178 this.code_mirror.on('keydown', $.proxy(this.handle_keyevent,this))
179 179 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
180 180 inner_cell.append(input_area);
181 181 input.append(prompt).append(inner_cell);
182 182
183 183 var widget_area = $('<div/>')
184 184 .addClass('widget-area')
185 185 .hide();
186 186 this.widget_area = widget_area;
187 187 var widget_prompt = $('<div/>')
188 188 .addClass('prompt')
189 189 .appendTo(widget_area);
190 190 var widget_subarea = $('<div/>')
191 191 .addClass('widget-subarea')
192 192 .appendTo(widget_area);
193 193 this.widget_subarea = widget_subarea;
194 var that = this;
194 195 var widget_clear_buton = $('<button />')
195 196 .addClass('close')
196 197 .html('&times;')
197 198 .click(function() {
198 widget_area.slideUp('', function(){ widget_subarea.html(''); });
199 })
199 widget_area.slideUp('', function(){
200 for (var i = 0; i < that.widget_views.length; i++) {
201 that.widget_views[i].remove();
202 }
203 that.widget_views = [];
204 widget_subarea.html('');
205 });
206 })
200 207 .appendTo(widget_prompt);
201 208
202 209 var output = $('<div></div>');
203 210 cell.append(input).append(widget_area).append(output);
204 211 this.element = cell;
205 212 this.output_area = new outputarea.OutputArea({
206 213 selector: output,
207 214 prompt_area: true,
208 215 events: this.events,
209 216 keyboard_manager: this.keyboard_manager});
210 217 this.completer = new completer.Completer(this, this.events);
211 218 };
212 219
220 /**
221 * Display a widget view in the cell.
222 */
223 CodeCell.prototype.display_widget_view = function(view_promise) {
224
225 // Display a dummy element
226 var dummy = $('<div/>');
227 this.widget_subarea.append(dummy);
228
229 // Display the view.
230 var that = this;
231 return view_promise.then(function(view) {
232 dummy.replaceWith(view.$el);
233 this.widget_views.push(view);
234 return view;
235 });
236 };
237
213 238 /** @method bind_events */
214 239 CodeCell.prototype.bind_events = function () {
215 240 Cell.prototype.bind_events.apply(this);
216 241 var that = this;
217 242
218 243 this.element.focusout(
219 244 function() { that.auto_highlight(); }
220 245 );
221 246 };
222 247
223 248
224 249 /**
225 250 * This method gets called in CodeMirror's onKeyDown/onKeyPress
226 251 * handlers and is used to provide custom key handling. Its return
227 252 * value is used to determine if CodeMirror should ignore the event:
228 253 * true = ignore, false = don't ignore.
229 254 * @method handle_codemirror_keyevent
230 255 */
231 256
232 257 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
233 258
234 259 var that = this;
235 260 // whatever key is pressed, first, cancel the tooltip request before
236 261 // they are sent, and remove tooltip if any, except for tab again
237 262 var tooltip_closed = null;
238 263 if (event.type === 'keydown' && event.which != keycodes.tab ) {
239 264 tooltip_closed = this.tooltip.remove_and_cancel_tooltip();
240 265 }
241 266
242 267 var cur = editor.getCursor();
243 268 if (event.keyCode === keycodes.enter){
244 269 this.auto_highlight();
245 270 }
246 271
247 272 if (event.which === keycodes.down && event.type === 'keypress' && this.tooltip.time_before_tooltip >= 0) {
248 273 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
249 274 // browser and keyboard layout !
250 275 // Pressing '(' , request tooltip, don't forget to reappend it
251 276 // The second argument says to hide the tooltip if the docstring
252 277 // is actually empty
253 278 this.tooltip.pending(that, true);
254 279 } else if ( tooltip_closed && event.which === keycodes.esc && event.type === 'keydown') {
255 280 // If tooltip is active, cancel it. The call to
256 281 // remove_and_cancel_tooltip above doesn't pass, force=true.
257 282 // Because of this it won't actually close the tooltip
258 283 // if it is in sticky mode. Thus, we have to check again if it is open
259 284 // and close it with force=true.
260 285 if (!this.tooltip._hidden) {
261 286 this.tooltip.remove_and_cancel_tooltip(true);
262 287 }
263 288 // If we closed the tooltip, don't let CM or the global handlers
264 289 // handle this event.
265 290 event.codemirrorIgnore = true;
266 291 event.preventDefault();
267 292 return true;
268 293 } else if (event.keyCode === keycodes.tab && event.type === 'keydown' && event.shiftKey) {
269 294 if (editor.somethingSelected() || editor.getSelections().length !== 1){
270 295 var anchor = editor.getCursor("anchor");
271 296 var head = editor.getCursor("head");
272 297 if( anchor.line != head.line){
273 298 return false;
274 299 }
275 300 }
276 301 this.tooltip.request(that);
277 302 event.codemirrorIgnore = true;
278 303 event.preventDefault();
279 304 return true;
280 305 } else if (event.keyCode === keycodes.tab && event.type == 'keydown') {
281 306 // Tab completion.
282 307 this.tooltip.remove_and_cancel_tooltip();
283 308
284 309 // completion does not work on multicursor, it might be possible though in some cases
285 310 if (editor.somethingSelected() || editor.getSelections().length > 1) {
286 311 return false;
287 312 }
288 313 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
289 314 if (pre_cursor.trim() === "") {
290 315 // Don't autocomplete if the part of the line before the cursor
291 316 // is empty. In this case, let CodeMirror handle indentation.
292 317 return false;
293 318 } else {
294 319 event.codemirrorIgnore = true;
295 320 event.preventDefault();
296 321 this.completer.startCompletion();
297 322 return true;
298 323 }
299 324 }
300 325
301 326 // keyboard event wasn't one of those unique to code cells, let's see
302 327 // if it's one of the generic ones (i.e. check edit mode shortcuts)
303 328 return Cell.prototype.handle_codemirror_keyevent.apply(this, [editor, event]);
304 329 };
305 330
306 331 // Kernel related calls.
307 332
308 333 CodeCell.prototype.set_kernel = function (kernel) {
309 334 this.kernel = kernel;
310 335 };
311 336
312 337 /**
313 338 * Execute current code cell to the kernel
314 339 * @method execute
315 340 */
316 341 CodeCell.prototype.execute = function () {
317 342 if (!this.kernel || !this.kernel.is_connected()) {
318 343 console.log("Can't execute, kernel is not connected.");
319 344 return;
320 345 }
321 346
322 347 this.active_output_area.clear_output();
323 348
324 349 // Clear widget area
350 for (var i = 0; i < this.widget_views.length; i++) {
351 this.widget_views[i].remove();
352 }
353 this.widget_views = [];
325 354 this.widget_subarea.html('');
326 355 this.widget_subarea.height('');
327 356 this.widget_area.height('');
328 357 this.widget_area.hide();
329 358
330 359 this.set_input_prompt('*');
331 360 this.element.addClass("running");
332 361 if (this.last_msg_id) {
333 362 this.kernel.clear_callbacks_for_msg(this.last_msg_id);
334 363 }
335 364 var callbacks = this.get_callbacks();
336 365
337 366 var old_msg_id = this.last_msg_id;
338 367 this.last_msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
339 368 if (old_msg_id) {
340 369 delete CodeCell.msg_cells[old_msg_id];
341 370 }
342 371 CodeCell.msg_cells[this.last_msg_id] = this;
343 372 this.render();
344 373 this.events.trigger('execute.CodeCell', {cell: this});
345 374 };
346 375
347 376 /**
348 377 * Construct the default callbacks for
349 378 * @method get_callbacks
350 379 */
351 380 CodeCell.prototype.get_callbacks = function () {
352 381 var that = this;
353 382 return {
354 383 shell : {
355 384 reply : $.proxy(this._handle_execute_reply, this),
356 385 payload : {
357 386 set_next_input : $.proxy(this._handle_set_next_input, this),
358 387 page : $.proxy(this._open_with_pager, this)
359 388 }
360 389 },
361 390 iopub : {
362 391 output : function() {
363 392 that.active_output_area.handle_output.apply(that.active_output_area, arguments);
364 393 },
365 394 clear_output : function() {
366 395 that.active_output_area.handle_clear_output.apply(that.active_output_area, arguments);
367 396 },
368 397 },
369 398 input : $.proxy(this._handle_input_request, this)
370 399 };
371 400 };
372 401
373 402 CodeCell.prototype._open_with_pager = function (payload) {
374 403 this.events.trigger('open_with_text.Pager', payload);
375 404 };
376 405
377 406 /**
378 407 * @method _handle_execute_reply
379 408 * @private
380 409 */
381 410 CodeCell.prototype._handle_execute_reply = function (msg) {
382 411 this.set_input_prompt(msg.content.execution_count);
383 412 this.element.removeClass("running");
384 413 this.events.trigger('set_dirty.Notebook', {value: true});
385 414 };
386 415
387 416 /**
388 417 * @method _handle_set_next_input
389 418 * @private
390 419 */
391 420 CodeCell.prototype._handle_set_next_input = function (payload) {
392 421 var data = {'cell': this, 'text': payload.text, replace: payload.replace};
393 422 this.events.trigger('set_next_input.Notebook', data);
394 423 };
395 424
396 425 /**
397 426 * @method _handle_input_request
398 427 * @private
399 428 */
400 429 CodeCell.prototype._handle_input_request = function (msg) {
401 430 this.active_output_area.append_raw_input(msg);
402 431 };
403 432
404 433
405 434 // Basic cell manipulation.
406 435
407 436 CodeCell.prototype.select = function () {
408 437 var cont = Cell.prototype.select.apply(this);
409 438 if (cont) {
410 439 this.code_mirror.refresh();
411 440 this.auto_highlight();
412 441 }
413 442 return cont;
414 443 };
415 444
416 445 CodeCell.prototype.render = function () {
417 446 var cont = Cell.prototype.render.apply(this);
418 447 // Always execute, even if we are already in the rendered state
419 448 return cont;
420 449 };
421 450
422 451 CodeCell.prototype.select_all = function () {
423 452 var start = {line: 0, ch: 0};
424 453 var nlines = this.code_mirror.lineCount();
425 454 var last_line = this.code_mirror.getLine(nlines-1);
426 455 var end = {line: nlines-1, ch: last_line.length};
427 456 this.code_mirror.setSelection(start, end);
428 457 };
429 458
430 459
431 460 CodeCell.prototype.collapse_output = function () {
432 461 this.output_area.collapse();
433 462 };
434 463
435 464
436 465 CodeCell.prototype.expand_output = function () {
437 466 this.output_area.expand();
438 467 this.output_area.unscroll_area();
439 468 };
440 469
441 470 CodeCell.prototype.scroll_output = function () {
442 471 this.output_area.expand();
443 472 this.output_area.scroll_if_long();
444 473 };
445 474
446 475 CodeCell.prototype.toggle_output = function () {
447 476 this.output_area.toggle_output();
448 477 };
449 478
450 479 CodeCell.prototype.toggle_output_scroll = function () {
451 480 this.output_area.toggle_scroll();
452 481 };
453 482
454 483
455 484 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
456 485 var ns;
457 486 if (prompt_value === undefined || prompt_value === null) {
458 487 ns = "&nbsp;";
459 488 } else {
460 489 ns = encodeURIComponent(prompt_value);
461 490 }
462 491 return 'In&nbsp;[' + ns + ']:';
463 492 };
464 493
465 494 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
466 495 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
467 496 for(var i=1; i < lines_number; i++) {
468 497 html.push(['...:']);
469 498 }
470 499 return html.join('<br/>');
471 500 };
472 501
473 502 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
474 503
475 504
476 505 CodeCell.prototype.set_input_prompt = function (number) {
477 506 var nline = 1;
478 507 if (this.code_mirror !== undefined) {
479 508 nline = this.code_mirror.lineCount();
480 509 }
481 510 this.input_prompt_number = number;
482 511 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
483 512 // This HTML call is okay because the user contents are escaped.
484 513 this.element.find('div.input_prompt').html(prompt_html);
485 514 };
486 515
487 516
488 517 CodeCell.prototype.clear_input = function () {
489 518 this.code_mirror.setValue('');
490 519 };
491 520
492 521
493 522 CodeCell.prototype.get_text = function () {
494 523 return this.code_mirror.getValue();
495 524 };
496 525
497 526
498 527 CodeCell.prototype.set_text = function (code) {
499 528 return this.code_mirror.setValue(code);
500 529 };
501 530
502 531
503 532 CodeCell.prototype.clear_output = function (wait) {
504 533 this.active_output_area.clear_output(wait);
505 534 this.set_input_prompt();
506 535 };
507 536
508 537
509 538 // JSON serialization
510 539
511 540 CodeCell.prototype.fromJSON = function (data) {
512 541 Cell.prototype.fromJSON.apply(this, arguments);
513 542 if (data.cell_type === 'code') {
514 543 if (data.source !== undefined) {
515 544 this.set_text(data.source);
516 545 // make this value the starting point, so that we can only undo
517 546 // to this state, instead of a blank cell
518 547 this.code_mirror.clearHistory();
519 548 this.auto_highlight();
520 549 }
521 550 this.set_input_prompt(data.execution_count);
522 551 this.output_area.trusted = data.metadata.trusted || false;
523 552 this.output_area.fromJSON(data.outputs);
524 553 if (data.metadata.collapsed !== undefined) {
525 554 if (data.metadata.collapsed) {
526 555 this.collapse_output();
527 556 } else {
528 557 this.expand_output();
529 558 }
530 559 }
531 560 }
532 561 };
533 562
534 563
535 564 CodeCell.prototype.toJSON = function () {
536 565 var data = Cell.prototype.toJSON.apply(this);
537 566 data.source = this.get_text();
538 567 // is finite protect against undefined and '*' value
539 568 if (isFinite(this.input_prompt_number)) {
540 569 data.execution_count = this.input_prompt_number;
541 570 } else {
542 571 data.execution_count = null;
543 572 }
544 573 var outputs = this.output_area.toJSON();
545 574 data.outputs = outputs;
546 575 data.metadata.trusted = this.output_area.trusted;
547 576 data.metadata.collapsed = this.output_area.collapsed;
548 577 return data;
549 578 };
550 579
551 580 /**
552 581 * handle cell level logic when a cell is unselected
553 582 * @method unselect
554 583 * @return is the action being taken
555 584 */
556 585 CodeCell.prototype.unselect = function () {
557 586 var cont = Cell.prototype.unselect.apply(this);
558 587 if (cont) {
559 588 // When a code cell is usnelected, make sure that the corresponding
560 589 // tooltip and completer to that cell is closed.
561 590 this.tooltip.remove_and_cancel_tooltip(true);
562 591 if (this.completer !== null) {
563 592 this.completer.close();
564 593 }
565 594 }
566 595 return cont;
567 596 };
568 597
569 598 // Backwards compatability.
570 599 IPython.CodeCell = CodeCell;
571 600
572 601 return {'CodeCell': CodeCell};
573 602 });
@@ -1,2526 +1,2533 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 'base/js/namespace',
6 6 'jquery',
7 7 'base/js/utils',
8 8 'base/js/dialog',
9 9 'notebook/js/cell',
10 10 'notebook/js/textcell',
11 11 'notebook/js/codecell',
12 12 'services/sessions/session',
13 13 'notebook/js/celltoolbar',
14 14 'components/marked/lib/marked',
15 15 'codemirror/lib/codemirror',
16 16 'codemirror/addon/runmode/runmode',
17 17 'notebook/js/mathjaxutils',
18 18 'base/js/keyboard',
19 19 'notebook/js/tooltip',
20 20 'notebook/js/celltoolbarpresets/default',
21 21 'notebook/js/celltoolbarpresets/rawcell',
22 22 'notebook/js/celltoolbarpresets/slideshow',
23 23 'notebook/js/scrollmanager'
24 24 ], function (
25 25 IPython,
26 26 $,
27 27 utils,
28 28 dialog,
29 29 cellmod,
30 30 textcell,
31 31 codecell,
32 32 session,
33 33 celltoolbar,
34 34 marked,
35 35 CodeMirror,
36 36 runMode,
37 37 mathjaxutils,
38 38 keyboard,
39 39 tooltip,
40 40 default_celltoolbar,
41 41 rawcell_celltoolbar,
42 42 slideshow_celltoolbar,
43 43 scrollmanager
44 44 ) {
45 45 "use strict";
46 46
47 47 var Notebook = function (selector, options) {
48 48 /**
49 49 * Constructor
50 50 *
51 51 * A notebook contains and manages cells.
52 52 *
53 53 * Parameters:
54 54 * selector: string
55 55 * options: dictionary
56 56 * Dictionary of keyword arguments.
57 57 * events: $(Events) instance
58 58 * keyboard_manager: KeyboardManager instance
59 59 * contents: Contents instance
60 60 * save_widget: SaveWidget instance
61 61 * config: dictionary
62 62 * base_url : string
63 63 * notebook_path : string
64 64 * notebook_name : string
65 65 */
66 66 this.config = utils.mergeopt(Notebook, options.config);
67 67 this.base_url = options.base_url;
68 68 this.notebook_path = options.notebook_path;
69 69 this.notebook_name = options.notebook_name;
70 70 this.events = options.events;
71 71 this.keyboard_manager = options.keyboard_manager;
72 72 this.contents = options.contents;
73 73 this.save_widget = options.save_widget;
74 74 this.tooltip = new tooltip.Tooltip(this.events);
75 75 this.ws_url = options.ws_url;
76 76 this._session_starting = false;
77 77 this.default_cell_type = this.config.default_cell_type || 'code';
78 78
79 79 // Create default scroll manager.
80 80 this.scroll_manager = new scrollmanager.ScrollManager(this);
81 81
82 82 // TODO: This code smells (and the other `= this` line a couple lines down)
83 83 // We need a better way to deal with circular instance references.
84 84 this.keyboard_manager.notebook = this;
85 85 this.save_widget.notebook = this;
86 86
87 87 mathjaxutils.init();
88 88
89 89 if (marked) {
90 90 marked.setOptions({
91 91 gfm : true,
92 92 tables: true,
93 93 // FIXME: probably want central config for CodeMirror theme when we have js config
94 94 langPrefix: "cm-s-ipython language-",
95 95 highlight: function(code, lang, callback) {
96 96 if (!lang) {
97 97 // no language, no highlight
98 98 if (callback) {
99 99 callback(null, code);
100 100 return;
101 101 } else {
102 102 return code;
103 103 }
104 104 }
105 105 utils.requireCodeMirrorMode(lang, function (spec) {
106 106 var el = document.createElement("div");
107 107 var mode = CodeMirror.getMode({}, spec);
108 108 if (!mode) {
109 109 console.log("No CodeMirror mode: " + lang);
110 110 callback(null, code);
111 111 return;
112 112 }
113 113 try {
114 114 CodeMirror.runMode(code, spec, el);
115 115 callback(null, el.innerHTML);
116 116 } catch (err) {
117 117 console.log("Failed to highlight " + lang + " code", err);
118 118 callback(err, code);
119 119 }
120 120 }, function (err) {
121 121 console.log("No CodeMirror mode: " + lang);
122 122 callback(err, code);
123 123 });
124 124 }
125 125 });
126 126 }
127 127
128 128 this.element = $(selector);
129 129 this.element.scroll();
130 130 this.element.data("notebook", this);
131 131 this.next_prompt_number = 1;
132 132 this.session = null;
133 133 this.kernel = null;
134 134 this.clipboard = null;
135 135 this.undelete_backup = null;
136 136 this.undelete_index = null;
137 137 this.undelete_below = false;
138 138 this.paste_enabled = false;
139 139 this.writable = false;
140 140 // It is important to start out in command mode to match the intial mode
141 141 // of the KeyboardManager.
142 142 this.mode = 'command';
143 143 this.set_dirty(false);
144 144 this.metadata = {};
145 145 this._checkpoint_after_save = false;
146 146 this.last_checkpoint = null;
147 147 this.checkpoints = [];
148 148 this.autosave_interval = 0;
149 149 this.autosave_timer = null;
150 150 // autosave *at most* every two minutes
151 151 this.minimum_autosave_interval = 120000;
152 152 this.notebook_name_blacklist_re = /[\/\\:]/;
153 153 this.nbformat = 4; // Increment this when changing the nbformat
154 154 this.nbformat_minor = this.current_nbformat_minor = 0; // Increment this when changing the nbformat
155 155 this.codemirror_mode = 'ipython';
156 156 this.create_elements();
157 157 this.bind_events();
158 158 this.kernel_selector = null;
159 159 this.dirty = null;
160 160 this.trusted = null;
161 161 this._fully_loaded = false;
162 162
163 163 // Trigger cell toolbar registration.
164 164 default_celltoolbar.register(this);
165 165 rawcell_celltoolbar.register(this);
166 166 slideshow_celltoolbar.register(this);
167 167
168 168 // prevent assign to miss-typed properties.
169 169 Object.seal(this);
170 170 };
171 171
172 172 Notebook.options_default = {
173 173 // can be any cell type, or the special values of
174 174 // 'above', 'below', or 'selected' to get the value from another cell.
175 175 Notebook: {
176 176 default_cell_type: 'code'
177 177 }
178 178 };
179 179
180 180
181 181 /**
182 182 * Create an HTML and CSS representation of the notebook.
183 183 *
184 184 * @method create_elements
185 185 */
186 186 Notebook.prototype.create_elements = function () {
187 187 var that = this;
188 188 this.element.attr('tabindex','-1');
189 189 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
190 190 // We add this end_space div to the end of the notebook div to:
191 191 // i) provide a margin between the last cell and the end of the notebook
192 192 // ii) to prevent the div from scrolling up when the last cell is being
193 193 // edited, but is too low on the page, which browsers will do automatically.
194 194 var end_space = $('<div/>').addClass('end_space');
195 195 end_space.dblclick(function (e) {
196 196 var ncells = that.ncells();
197 197 that.insert_cell_below('code',ncells-1);
198 198 });
199 199 this.element.append(this.container);
200 200 this.container.append(end_space);
201 201 };
202 202
203 203 /**
204 204 * Bind JavaScript events: key presses and custom IPython events.
205 205 *
206 206 * @method bind_events
207 207 */
208 208 Notebook.prototype.bind_events = function () {
209 209 var that = this;
210 210
211 211 this.events.on('set_next_input.Notebook', function (event, data) {
212 212 if (data.replace) {
213 213 data.cell.set_text(data.text);
214 214 data.cell.clear_output();
215 215 } else {
216 216 var index = that.find_cell_index(data.cell);
217 217 var new_cell = that.insert_cell_below('code',index);
218 218 new_cell.set_text(data.text);
219 219 }
220 220 that.dirty = true;
221 221 });
222 222
223 223 this.events.on('unrecognized_cell.Cell', function () {
224 224 that.warn_nbformat_minor();
225 225 });
226 226
227 227 this.events.on('unrecognized_output.OutputArea', function () {
228 228 that.warn_nbformat_minor();
229 229 });
230 230
231 231 this.events.on('set_dirty.Notebook', function (event, data) {
232 232 that.dirty = data.value;
233 233 });
234 234
235 235 this.events.on('trust_changed.Notebook', function (event, trusted) {
236 236 that.trusted = trusted;
237 237 });
238 238
239 239 this.events.on('select.Cell', function (event, data) {
240 240 var index = that.find_cell_index(data.cell);
241 241 that.select(index);
242 242 });
243 243
244 244 this.events.on('edit_mode.Cell', function (event, data) {
245 245 that.handle_edit_mode(data.cell);
246 246 });
247 247
248 248 this.events.on('command_mode.Cell', function (event, data) {
249 249 that.handle_command_mode(data.cell);
250 250 });
251 251
252 252 this.events.on('spec_changed.Kernel', function(event, data) {
253 253 that.metadata.kernelspec =
254 254 {name: data.name, display_name: data.display_name};
255 255 });
256 256
257 257 this.events.on('kernel_ready.Kernel', function(event, data) {
258 258 var kinfo = data.kernel.info_reply;
259 259 var langinfo = kinfo.language_info || {};
260 260 that.metadata.language_info = langinfo;
261 261 // Mode 'null' should be plain, unhighlighted text.
262 262 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
263 263 that.set_codemirror_mode(cm_mode);
264 264 });
265 265
266 266 var collapse_time = function (time) {
267 267 var app_height = $('#ipython-main-app').height(); // content height
268 268 var splitter_height = $('div#pager_splitter').outerHeight(true);
269 269 var new_height = app_height - splitter_height;
270 270 that.element.animate({height : new_height + 'px'}, time);
271 271 };
272 272
273 273 this.element.bind('collapse_pager', function (event, extrap) {
274 274 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
275 275 collapse_time(time);
276 276 });
277 277
278 278 var expand_time = function (time) {
279 279 var app_height = $('#ipython-main-app').height(); // content height
280 280 var splitter_height = $('div#pager_splitter').outerHeight(true);
281 281 var pager_height = $('div#pager').outerHeight(true);
282 282 var new_height = app_height - pager_height - splitter_height;
283 283 that.element.animate({height : new_height + 'px'}, time);
284 284 };
285 285
286 286 this.element.bind('expand_pager', function (event, extrap) {
287 287 var time = (extrap !== undefined) ? ((extrap.duration !== undefined ) ? extrap.duration : 'fast') : 'fast';
288 288 expand_time(time);
289 289 });
290 290
291 291 // Firefox 22 broke $(window).on("beforeunload")
292 292 // I'm not sure why or how.
293 293 window.onbeforeunload = function (e) {
294 // Raise an event that allows the user to execute custom code on unload
295 try {
296 that.events.trigger('beforeunload.Notebook', {notebook: that});
297 } catch(e) {
298 console.err('Error in "beforeunload.Notebook" event handler.', e);
299 }
300
294 301 // TODO: Make killing the kernel configurable.
295 302 var kill_kernel = false;
296 303 if (kill_kernel) {
297 304 that.session.delete();
298 305 }
299 306 // if we are autosaving, trigger an autosave on nav-away.
300 307 // still warn, because if we don't the autosave may fail.
301 308 if (that.dirty) {
302 309 if ( that.autosave_interval ) {
303 310 // schedule autosave in a timeout
304 311 // this gives you a chance to forcefully discard changes
305 312 // by reloading the page if you *really* want to.
306 313 // the timer doesn't start until you *dismiss* the dialog.
307 314 setTimeout(function () {
308 315 if (that.dirty) {
309 316 that.save_notebook();
310 317 }
311 318 }, 1000);
312 319 return "Autosave in progress, latest changes may be lost.";
313 320 } else {
314 321 return "Unsaved changes will be lost.";
315 322 }
316 323 }
317 324 // Null is the *only* return value that will make the browser not
318 325 // pop up the "don't leave" dialog.
319 326 return null;
320 327 };
321 328 };
322 329
323 330 Notebook.prototype.warn_nbformat_minor = function (event) {
324 331 /**
325 332 * trigger a warning dialog about missing functionality from newer minor versions
326 333 */
327 334 var v = 'v' + this.nbformat + '.';
328 335 var orig_vs = v + this.nbformat_minor;
329 336 var this_vs = v + this.current_nbformat_minor;
330 337 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
331 338 this_vs + ". You can still work with this notebook, but cell and output types " +
332 339 "introduced in later notebook versions will not be available.";
333 340
334 341 dialog.modal({
335 342 notebook: this,
336 343 keyboard_manager: this.keyboard_manager,
337 344 title : "Newer Notebook",
338 345 body : msg,
339 346 buttons : {
340 347 OK : {
341 348 "class" : "btn-danger"
342 349 }
343 350 }
344 351 });
345 352 }
346 353
347 354 /**
348 355 * Set the dirty flag, and trigger the set_dirty.Notebook event
349 356 *
350 357 * @method set_dirty
351 358 */
352 359 Notebook.prototype.set_dirty = function (value) {
353 360 if (value === undefined) {
354 361 value = true;
355 362 }
356 363 if (this.dirty == value) {
357 364 return;
358 365 }
359 366 this.events.trigger('set_dirty.Notebook', {value: value});
360 367 };
361 368
362 369 /**
363 370 * Scroll the top of the page to a given cell.
364 371 *
365 372 * @method scroll_to_cell
366 373 * @param {Number} cell_number An index of the cell to view
367 374 * @param {Number} time Animation time in milliseconds
368 375 * @return {Number} Pixel offset from the top of the container
369 376 */
370 377 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
371 378 var cells = this.get_cells();
372 379 time = time || 0;
373 380 cell_number = Math.min(cells.length-1,cell_number);
374 381 cell_number = Math.max(0 ,cell_number);
375 382 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
376 383 this.element.animate({scrollTop:scroll_value}, time);
377 384 return scroll_value;
378 385 };
379 386
380 387 /**
381 388 * Scroll to the bottom of the page.
382 389 *
383 390 * @method scroll_to_bottom
384 391 */
385 392 Notebook.prototype.scroll_to_bottom = function () {
386 393 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
387 394 };
388 395
389 396 /**
390 397 * Scroll to the top of the page.
391 398 *
392 399 * @method scroll_to_top
393 400 */
394 401 Notebook.prototype.scroll_to_top = function () {
395 402 this.element.animate({scrollTop:0}, 0);
396 403 };
397 404
398 405 // Edit Notebook metadata
399 406
400 407 Notebook.prototype.edit_metadata = function () {
401 408 var that = this;
402 409 dialog.edit_metadata({
403 410 md: this.metadata,
404 411 callback: function (md) {
405 412 that.metadata = md;
406 413 },
407 414 name: 'Notebook',
408 415 notebook: this,
409 416 keyboard_manager: this.keyboard_manager});
410 417 };
411 418
412 419 // Cell indexing, retrieval, etc.
413 420
414 421 /**
415 422 * Get all cell elements in the notebook.
416 423 *
417 424 * @method get_cell_elements
418 425 * @return {jQuery} A selector of all cell elements
419 426 */
420 427 Notebook.prototype.get_cell_elements = function () {
421 428 return this.container.find(".cell").not('.cell .cell');
422 429 };
423 430
424 431 /**
425 432 * Get a particular cell element.
426 433 *
427 434 * @method get_cell_element
428 435 * @param {Number} index An index of a cell to select
429 436 * @return {jQuery} A selector of the given cell.
430 437 */
431 438 Notebook.prototype.get_cell_element = function (index) {
432 439 var result = null;
433 440 var e = this.get_cell_elements().eq(index);
434 441 if (e.length !== 0) {
435 442 result = e;
436 443 }
437 444 return result;
438 445 };
439 446
440 447 /**
441 448 * Try to get a particular cell by msg_id.
442 449 *
443 450 * @method get_msg_cell
444 451 * @param {String} msg_id A message UUID
445 452 * @return {Cell} Cell or null if no cell was found.
446 453 */
447 454 Notebook.prototype.get_msg_cell = function (msg_id) {
448 455 return codecell.CodeCell.msg_cells[msg_id] || null;
449 456 };
450 457
451 458 /**
452 459 * Count the cells in this notebook.
453 460 *
454 461 * @method ncells
455 462 * @return {Number} The number of cells in this notebook
456 463 */
457 464 Notebook.prototype.ncells = function () {
458 465 return this.get_cell_elements().length;
459 466 };
460 467
461 468 /**
462 469 * Get all Cell objects in this notebook.
463 470 *
464 471 * @method get_cells
465 472 * @return {Array} This notebook's Cell objects
466 473 */
467 474 // TODO: we are often calling cells as cells()[i], which we should optimize
468 475 // to cells(i) or a new method.
469 476 Notebook.prototype.get_cells = function () {
470 477 return this.get_cell_elements().toArray().map(function (e) {
471 478 return $(e).data("cell");
472 479 });
473 480 };
474 481
475 482 /**
476 483 * Get a Cell object from this notebook.
477 484 *
478 485 * @method get_cell
479 486 * @param {Number} index An index of a cell to retrieve
480 487 * @return {Cell} Cell or null if no cell was found.
481 488 */
482 489 Notebook.prototype.get_cell = function (index) {
483 490 var result = null;
484 491 var ce = this.get_cell_element(index);
485 492 if (ce !== null) {
486 493 result = ce.data('cell');
487 494 }
488 495 return result;
489 496 };
490 497
491 498 /**
492 499 * Get the cell below a given cell.
493 500 *
494 501 * @method get_next_cell
495 502 * @param {Cell} cell The provided cell
496 503 * @return {Cell} the next cell or null if no cell was found.
497 504 */
498 505 Notebook.prototype.get_next_cell = function (cell) {
499 506 var result = null;
500 507 var index = this.find_cell_index(cell);
501 508 if (this.is_valid_cell_index(index+1)) {
502 509 result = this.get_cell(index+1);
503 510 }
504 511 return result;
505 512 };
506 513
507 514 /**
508 515 * Get the cell above a given cell.
509 516 *
510 517 * @method get_prev_cell
511 518 * @param {Cell} cell The provided cell
512 519 * @return {Cell} The previous cell or null if no cell was found.
513 520 */
514 521 Notebook.prototype.get_prev_cell = function (cell) {
515 522 var result = null;
516 523 var index = this.find_cell_index(cell);
517 524 if (index !== null && index > 0) {
518 525 result = this.get_cell(index-1);
519 526 }
520 527 return result;
521 528 };
522 529
523 530 /**
524 531 * Get the numeric index of a given cell.
525 532 *
526 533 * @method find_cell_index
527 534 * @param {Cell} cell The provided cell
528 535 * @return {Number} The cell's numeric index or null if no cell was found.
529 536 */
530 537 Notebook.prototype.find_cell_index = function (cell) {
531 538 var result = null;
532 539 this.get_cell_elements().filter(function (index) {
533 540 if ($(this).data("cell") === cell) {
534 541 result = index;
535 542 }
536 543 });
537 544 return result;
538 545 };
539 546
540 547 /**
541 548 * Get a given index , or the selected index if none is provided.
542 549 *
543 550 * @method index_or_selected
544 551 * @param {Number} index A cell's index
545 552 * @return {Number} The given index, or selected index if none is provided.
546 553 */
547 554 Notebook.prototype.index_or_selected = function (index) {
548 555 var i;
549 556 if (index === undefined || index === null) {
550 557 i = this.get_selected_index();
551 558 if (i === null) {
552 559 i = 0;
553 560 }
554 561 } else {
555 562 i = index;
556 563 }
557 564 return i;
558 565 };
559 566
560 567 /**
561 568 * Get the currently selected cell.
562 569 * @method get_selected_cell
563 570 * @return {Cell} The selected cell
564 571 */
565 572 Notebook.prototype.get_selected_cell = function () {
566 573 var index = this.get_selected_index();
567 574 return this.get_cell(index);
568 575 };
569 576
570 577 /**
571 578 * Check whether a cell index is valid.
572 579 *
573 580 * @method is_valid_cell_index
574 581 * @param {Number} index A cell index
575 582 * @return True if the index is valid, false otherwise
576 583 */
577 584 Notebook.prototype.is_valid_cell_index = function (index) {
578 585 if (index !== null && index >= 0 && index < this.ncells()) {
579 586 return true;
580 587 } else {
581 588 return false;
582 589 }
583 590 };
584 591
585 592 /**
586 593 * Get the index of the currently selected cell.
587 594
588 595 * @method get_selected_index
589 596 * @return {Number} The selected cell's numeric index
590 597 */
591 598 Notebook.prototype.get_selected_index = function () {
592 599 var result = null;
593 600 this.get_cell_elements().filter(function (index) {
594 601 if ($(this).data("cell").selected === true) {
595 602 result = index;
596 603 }
597 604 });
598 605 return result;
599 606 };
600 607
601 608
602 609 // Cell selection.
603 610
604 611 /**
605 612 * Programmatically select a cell.
606 613 *
607 614 * @method select
608 615 * @param {Number} index A cell's index
609 616 * @return {Notebook} This notebook
610 617 */
611 618 Notebook.prototype.select = function (index) {
612 619 if (this.is_valid_cell_index(index)) {
613 620 var sindex = this.get_selected_index();
614 621 if (sindex !== null && index !== sindex) {
615 622 // If we are about to select a different cell, make sure we are
616 623 // first in command mode.
617 624 if (this.mode !== 'command') {
618 625 this.command_mode();
619 626 }
620 627 this.get_cell(sindex).unselect();
621 628 }
622 629 var cell = this.get_cell(index);
623 630 cell.select();
624 631 if (cell.cell_type === 'heading') {
625 632 this.events.trigger('selected_cell_type_changed.Notebook',
626 633 {'cell_type':cell.cell_type,level:cell.level}
627 634 );
628 635 } else {
629 636 this.events.trigger('selected_cell_type_changed.Notebook',
630 637 {'cell_type':cell.cell_type}
631 638 );
632 639 }
633 640 }
634 641 return this;
635 642 };
636 643
637 644 /**
638 645 * Programmatically select the next cell.
639 646 *
640 647 * @method select_next
641 648 * @return {Notebook} This notebook
642 649 */
643 650 Notebook.prototype.select_next = function () {
644 651 var index = this.get_selected_index();
645 652 this.select(index+1);
646 653 return this;
647 654 };
648 655
649 656 /**
650 657 * Programmatically select the previous cell.
651 658 *
652 659 * @method select_prev
653 660 * @return {Notebook} This notebook
654 661 */
655 662 Notebook.prototype.select_prev = function () {
656 663 var index = this.get_selected_index();
657 664 this.select(index-1);
658 665 return this;
659 666 };
660 667
661 668
662 669 // Edit/Command mode
663 670
664 671 /**
665 672 * Gets the index of the cell that is in edit mode.
666 673 *
667 674 * @method get_edit_index
668 675 *
669 676 * @return index {int}
670 677 **/
671 678 Notebook.prototype.get_edit_index = function () {
672 679 var result = null;
673 680 this.get_cell_elements().filter(function (index) {
674 681 if ($(this).data("cell").mode === 'edit') {
675 682 result = index;
676 683 }
677 684 });
678 685 return result;
679 686 };
680 687
681 688 /**
682 689 * Handle when a a cell blurs and the notebook should enter command mode.
683 690 *
684 691 * @method handle_command_mode
685 692 * @param [cell] {Cell} Cell to enter command mode on.
686 693 **/
687 694 Notebook.prototype.handle_command_mode = function (cell) {
688 695 if (this.mode !== 'command') {
689 696 cell.command_mode();
690 697 this.mode = 'command';
691 698 this.events.trigger('command_mode.Notebook');
692 699 this.keyboard_manager.command_mode();
693 700 }
694 701 };
695 702
696 703 /**
697 704 * Make the notebook enter command mode.
698 705 *
699 706 * @method command_mode
700 707 **/
701 708 Notebook.prototype.command_mode = function () {
702 709 var cell = this.get_cell(this.get_edit_index());
703 710 if (cell && this.mode !== 'command') {
704 711 // We don't call cell.command_mode, but rather call cell.focus_cell()
705 712 // which will blur and CM editor and trigger the call to
706 713 // handle_command_mode.
707 714 cell.focus_cell();
708 715 }
709 716 };
710 717
711 718 /**
712 719 * Handle when a cell fires it's edit_mode event.
713 720 *
714 721 * @method handle_edit_mode
715 722 * @param [cell] {Cell} Cell to enter edit mode on.
716 723 **/
717 724 Notebook.prototype.handle_edit_mode = function (cell) {
718 725 if (cell && this.mode !== 'edit') {
719 726 cell.edit_mode();
720 727 this.mode = 'edit';
721 728 this.events.trigger('edit_mode.Notebook');
722 729 this.keyboard_manager.edit_mode();
723 730 }
724 731 };
725 732
726 733 /**
727 734 * Make a cell enter edit mode.
728 735 *
729 736 * @method edit_mode
730 737 **/
731 738 Notebook.prototype.edit_mode = function () {
732 739 var cell = this.get_selected_cell();
733 740 if (cell && this.mode !== 'edit') {
734 741 cell.unrender();
735 742 cell.focus_editor();
736 743 }
737 744 };
738 745
739 746 /**
740 747 * Focus the currently selected cell.
741 748 *
742 749 * @method focus_cell
743 750 **/
744 751 Notebook.prototype.focus_cell = function () {
745 752 var cell = this.get_selected_cell();
746 753 if (cell === null) {return;} // No cell is selected
747 754 cell.focus_cell();
748 755 };
749 756
750 757 // Cell movement
751 758
752 759 /**
753 760 * Move given (or selected) cell up and select it.
754 761 *
755 762 * @method move_cell_up
756 763 * @param [index] {integer} cell index
757 764 * @return {Notebook} This notebook
758 765 **/
759 766 Notebook.prototype.move_cell_up = function (index) {
760 767 var i = this.index_or_selected(index);
761 768 if (this.is_valid_cell_index(i) && i > 0) {
762 769 var pivot = this.get_cell_element(i-1);
763 770 var tomove = this.get_cell_element(i);
764 771 if (pivot !== null && tomove !== null) {
765 772 tomove.detach();
766 773 pivot.before(tomove);
767 774 this.select(i-1);
768 775 var cell = this.get_selected_cell();
769 776 cell.focus_cell();
770 777 }
771 778 this.set_dirty(true);
772 779 }
773 780 return this;
774 781 };
775 782
776 783
777 784 /**
778 785 * Move given (or selected) cell down and select it
779 786 *
780 787 * @method move_cell_down
781 788 * @param [index] {integer} cell index
782 789 * @return {Notebook} This notebook
783 790 **/
784 791 Notebook.prototype.move_cell_down = function (index) {
785 792 var i = this.index_or_selected(index);
786 793 if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
787 794 var pivot = this.get_cell_element(i+1);
788 795 var tomove = this.get_cell_element(i);
789 796 if (pivot !== null && tomove !== null) {
790 797 tomove.detach();
791 798 pivot.after(tomove);
792 799 this.select(i+1);
793 800 var cell = this.get_selected_cell();
794 801 cell.focus_cell();
795 802 }
796 803 }
797 804 this.set_dirty();
798 805 return this;
799 806 };
800 807
801 808
802 809 // Insertion, deletion.
803 810
804 811 /**
805 812 * Delete a cell from the notebook.
806 813 *
807 814 * @method delete_cell
808 815 * @param [index] A cell's numeric index
809 816 * @return {Notebook} This notebook
810 817 */
811 818 Notebook.prototype.delete_cell = function (index) {
812 819 var i = this.index_or_selected(index);
813 820 var cell = this.get_cell(i);
814 821 if (!cell.is_deletable()) {
815 822 return this;
816 823 }
817 824
818 825 this.undelete_backup = cell.toJSON();
819 826 $('#undelete_cell').removeClass('disabled');
820 827 if (this.is_valid_cell_index(i)) {
821 828 var old_ncells = this.ncells();
822 829 var ce = this.get_cell_element(i);
823 830 ce.remove();
824 831 if (i === 0) {
825 832 // Always make sure we have at least one cell.
826 833 if (old_ncells === 1) {
827 834 this.insert_cell_below('code');
828 835 }
829 836 this.select(0);
830 837 this.undelete_index = 0;
831 838 this.undelete_below = false;
832 839 } else if (i === old_ncells-1 && i !== 0) {
833 840 this.select(i-1);
834 841 this.undelete_index = i - 1;
835 842 this.undelete_below = true;
836 843 } else {
837 844 this.select(i);
838 845 this.undelete_index = i;
839 846 this.undelete_below = false;
840 847 }
841 848 this.events.trigger('delete.Cell', {'cell': cell, 'index': i});
842 849 this.set_dirty(true);
843 850 }
844 851 return this;
845 852 };
846 853
847 854 /**
848 855 * Restore the most recently deleted cell.
849 856 *
850 857 * @method undelete
851 858 */
852 859 Notebook.prototype.undelete_cell = function() {
853 860 if (this.undelete_backup !== null && this.undelete_index !== null) {
854 861 var current_index = this.get_selected_index();
855 862 if (this.undelete_index < current_index) {
856 863 current_index = current_index + 1;
857 864 }
858 865 if (this.undelete_index >= this.ncells()) {
859 866 this.select(this.ncells() - 1);
860 867 }
861 868 else {
862 869 this.select(this.undelete_index);
863 870 }
864 871 var cell_data = this.undelete_backup;
865 872 var new_cell = null;
866 873 if (this.undelete_below) {
867 874 new_cell = this.insert_cell_below(cell_data.cell_type);
868 875 } else {
869 876 new_cell = this.insert_cell_above(cell_data.cell_type);
870 877 }
871 878 new_cell.fromJSON(cell_data);
872 879 if (this.undelete_below) {
873 880 this.select(current_index+1);
874 881 } else {
875 882 this.select(current_index);
876 883 }
877 884 this.undelete_backup = null;
878 885 this.undelete_index = null;
879 886 }
880 887 $('#undelete_cell').addClass('disabled');
881 888 };
882 889
883 890 /**
884 891 * Insert a cell so that after insertion the cell is at given index.
885 892 *
886 893 * If cell type is not provided, it will default to the type of the
887 894 * currently active cell.
888 895 *
889 896 * Similar to insert_above, but index parameter is mandatory
890 897 *
891 898 * Index will be brought back into the accessible range [0,n]
892 899 *
893 900 * @method insert_cell_at_index
894 901 * @param [type] {string} in ['code','markdown', 'raw'], defaults to 'code'
895 902 * @param [index] {int} a valid index where to insert cell
896 903 *
897 904 * @return cell {cell|null} created cell or null
898 905 **/
899 906 Notebook.prototype.insert_cell_at_index = function(type, index){
900 907
901 908 var ncells = this.ncells();
902 909 index = Math.min(index, ncells);
903 910 index = Math.max(index, 0);
904 911 var cell = null;
905 912 type = type || this.default_cell_type;
906 913 if (type === 'above') {
907 914 if (index > 0) {
908 915 type = this.get_cell(index-1).cell_type;
909 916 } else {
910 917 type = 'code';
911 918 }
912 919 } else if (type === 'below') {
913 920 if (index < ncells) {
914 921 type = this.get_cell(index).cell_type;
915 922 } else {
916 923 type = 'code';
917 924 }
918 925 } else if (type === 'selected') {
919 926 type = this.get_selected_cell().cell_type;
920 927 }
921 928
922 929 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
923 930 var cell_options = {
924 931 events: this.events,
925 932 config: this.config,
926 933 keyboard_manager: this.keyboard_manager,
927 934 notebook: this,
928 935 tooltip: this.tooltip
929 936 };
930 937 switch(type) {
931 938 case 'code':
932 939 cell = new codecell.CodeCell(this.kernel, cell_options);
933 940 cell.set_input_prompt();
934 941 break;
935 942 case 'markdown':
936 943 cell = new textcell.MarkdownCell(cell_options);
937 944 break;
938 945 case 'raw':
939 946 cell = new textcell.RawCell(cell_options);
940 947 break;
941 948 default:
942 949 console.log("Unrecognized cell type: ", type, cellmod);
943 950 cell = new cellmod.UnrecognizedCell(cell_options);
944 951 }
945 952
946 953 if(this._insert_element_at_index(cell.element,index)) {
947 954 cell.render();
948 955 this.events.trigger('create.Cell', {'cell': cell, 'index': index});
949 956 cell.refresh();
950 957 // We used to select the cell after we refresh it, but there
951 958 // are now cases were this method is called where select is
952 959 // not appropriate. The selection logic should be handled by the
953 960 // caller of the the top level insert_cell methods.
954 961 this.set_dirty(true);
955 962 }
956 963 }
957 964 return cell;
958 965
959 966 };
960 967
961 968 /**
962 969 * Insert an element at given cell index.
963 970 *
964 971 * @method _insert_element_at_index
965 972 * @param element {dom_element} a cell element
966 973 * @param [index] {int} a valid index where to inser cell
967 974 * @private
968 975 *
969 976 * return true if everything whent fine.
970 977 **/
971 978 Notebook.prototype._insert_element_at_index = function(element, index){
972 979 if (element === undefined){
973 980 return false;
974 981 }
975 982
976 983 var ncells = this.ncells();
977 984
978 985 if (ncells === 0) {
979 986 // special case append if empty
980 987 this.element.find('div.end_space').before(element);
981 988 } else if ( ncells === index ) {
982 989 // special case append it the end, but not empty
983 990 this.get_cell_element(index-1).after(element);
984 991 } else if (this.is_valid_cell_index(index)) {
985 992 // otherwise always somewhere to append to
986 993 this.get_cell_element(index).before(element);
987 994 } else {
988 995 return false;
989 996 }
990 997
991 998 if (this.undelete_index !== null && index <= this.undelete_index) {
992 999 this.undelete_index = this.undelete_index + 1;
993 1000 this.set_dirty(true);
994 1001 }
995 1002 return true;
996 1003 };
997 1004
998 1005 /**
999 1006 * Insert a cell of given type above given index, or at top
1000 1007 * of notebook if index smaller than 0.
1001 1008 *
1002 1009 * default index value is the one of currently selected cell
1003 1010 *
1004 1011 * @method insert_cell_above
1005 1012 * @param [type] {string} cell type
1006 1013 * @param [index] {integer}
1007 1014 *
1008 1015 * @return handle to created cell or null
1009 1016 **/
1010 1017 Notebook.prototype.insert_cell_above = function (type, index) {
1011 1018 index = this.index_or_selected(index);
1012 1019 return this.insert_cell_at_index(type, index);
1013 1020 };
1014 1021
1015 1022 /**
1016 1023 * Insert a cell of given type below given index, or at bottom
1017 1024 * of notebook if index greater than number of cells
1018 1025 *
1019 1026 * default index value is the one of currently selected cell
1020 1027 *
1021 1028 * @method insert_cell_below
1022 1029 * @param [type] {string} cell type
1023 1030 * @param [index] {integer}
1024 1031 *
1025 1032 * @return handle to created cell or null
1026 1033 *
1027 1034 **/
1028 1035 Notebook.prototype.insert_cell_below = function (type, index) {
1029 1036 index = this.index_or_selected(index);
1030 1037 return this.insert_cell_at_index(type, index+1);
1031 1038 };
1032 1039
1033 1040
1034 1041 /**
1035 1042 * Insert cell at end of notebook
1036 1043 *
1037 1044 * @method insert_cell_at_bottom
1038 1045 * @param {String} type cell type
1039 1046 *
1040 1047 * @return the added cell; or null
1041 1048 **/
1042 1049 Notebook.prototype.insert_cell_at_bottom = function (type){
1043 1050 var len = this.ncells();
1044 1051 return this.insert_cell_below(type,len-1);
1045 1052 };
1046 1053
1047 1054 /**
1048 1055 * Turn a cell into a code cell.
1049 1056 *
1050 1057 * @method to_code
1051 1058 * @param {Number} [index] A cell's index
1052 1059 */
1053 1060 Notebook.prototype.to_code = function (index) {
1054 1061 var i = this.index_or_selected(index);
1055 1062 if (this.is_valid_cell_index(i)) {
1056 1063 var source_cell = this.get_cell(i);
1057 1064 if (!(source_cell instanceof codecell.CodeCell)) {
1058 1065 var target_cell = this.insert_cell_below('code',i);
1059 1066 var text = source_cell.get_text();
1060 1067 if (text === source_cell.placeholder) {
1061 1068 text = '';
1062 1069 }
1063 1070 //metadata
1064 1071 target_cell.metadata = source_cell.metadata;
1065 1072
1066 1073 target_cell.set_text(text);
1067 1074 // make this value the starting point, so that we can only undo
1068 1075 // to this state, instead of a blank cell
1069 1076 target_cell.code_mirror.clearHistory();
1070 1077 source_cell.element.remove();
1071 1078 this.select(i);
1072 1079 var cursor = source_cell.code_mirror.getCursor();
1073 1080 target_cell.code_mirror.setCursor(cursor);
1074 1081 this.set_dirty(true);
1075 1082 }
1076 1083 }
1077 1084 };
1078 1085
1079 1086 /**
1080 1087 * Turn a cell into a Markdown cell.
1081 1088 *
1082 1089 * @method to_markdown
1083 1090 * @param {Number} [index] A cell's index
1084 1091 */
1085 1092 Notebook.prototype.to_markdown = function (index) {
1086 1093 var i = this.index_or_selected(index);
1087 1094 if (this.is_valid_cell_index(i)) {
1088 1095 var source_cell = this.get_cell(i);
1089 1096
1090 1097 if (!(source_cell instanceof textcell.MarkdownCell)) {
1091 1098 var target_cell = this.insert_cell_below('markdown',i);
1092 1099 var text = source_cell.get_text();
1093 1100
1094 1101 if (text === source_cell.placeholder) {
1095 1102 text = '';
1096 1103 }
1097 1104 // metadata
1098 1105 target_cell.metadata = source_cell.metadata;
1099 1106 // We must show the editor before setting its contents
1100 1107 target_cell.unrender();
1101 1108 target_cell.set_text(text);
1102 1109 // make this value the starting point, so that we can only undo
1103 1110 // to this state, instead of a blank cell
1104 1111 target_cell.code_mirror.clearHistory();
1105 1112 source_cell.element.remove();
1106 1113 this.select(i);
1107 1114 if ((source_cell instanceof textcell.TextCell) && source_cell.rendered) {
1108 1115 target_cell.render();
1109 1116 }
1110 1117 var cursor = source_cell.code_mirror.getCursor();
1111 1118 target_cell.code_mirror.setCursor(cursor);
1112 1119 this.set_dirty(true);
1113 1120 }
1114 1121 }
1115 1122 };
1116 1123
1117 1124 /**
1118 1125 * Turn a cell into a raw text cell.
1119 1126 *
1120 1127 * @method to_raw
1121 1128 * @param {Number} [index] A cell's index
1122 1129 */
1123 1130 Notebook.prototype.to_raw = function (index) {
1124 1131 var i = this.index_or_selected(index);
1125 1132 if (this.is_valid_cell_index(i)) {
1126 1133 var target_cell = null;
1127 1134 var source_cell = this.get_cell(i);
1128 1135
1129 1136 if (!(source_cell instanceof textcell.RawCell)) {
1130 1137 target_cell = this.insert_cell_below('raw',i);
1131 1138 var text = source_cell.get_text();
1132 1139 if (text === source_cell.placeholder) {
1133 1140 text = '';
1134 1141 }
1135 1142 //metadata
1136 1143 target_cell.metadata = source_cell.metadata;
1137 1144 // We must show the editor before setting its contents
1138 1145 target_cell.unrender();
1139 1146 target_cell.set_text(text);
1140 1147 // make this value the starting point, so that we can only undo
1141 1148 // to this state, instead of a blank cell
1142 1149 target_cell.code_mirror.clearHistory();
1143 1150 source_cell.element.remove();
1144 1151 this.select(i);
1145 1152 var cursor = source_cell.code_mirror.getCursor();
1146 1153 target_cell.code_mirror.setCursor(cursor);
1147 1154 this.set_dirty(true);
1148 1155 }
1149 1156 }
1150 1157 };
1151 1158
1152 1159 Notebook.prototype._warn_heading = function () {
1153 1160 /**
1154 1161 * warn about heading cells being removed
1155 1162 */
1156 1163 dialog.modal({
1157 1164 notebook: this,
1158 1165 keyboard_manager: this.keyboard_manager,
1159 1166 title : "Use markdown headings",
1160 1167 body : $("<p/>").text(
1161 1168 'IPython no longer uses special heading cells. ' +
1162 1169 'Instead, write your headings in Markdown cells using # characters:'
1163 1170 ).append($('<pre/>').text(
1164 1171 '## This is a level 2 heading'
1165 1172 )),
1166 1173 buttons : {
1167 1174 "OK" : {}
1168 1175 }
1169 1176 });
1170 1177 };
1171 1178
1172 1179 /**
1173 1180 * Turn a cell into a markdown cell with a heading.
1174 1181 *
1175 1182 * @method to_heading
1176 1183 * @param {Number} [index] A cell's index
1177 1184 * @param {Number} [level] A heading level (e.g., 1 for h1)
1178 1185 */
1179 1186 Notebook.prototype.to_heading = function (index, level) {
1180 1187 this.to_markdown(index);
1181 1188 level = level || 1;
1182 1189 var i = this.index_or_selected(index);
1183 1190 if (this.is_valid_cell_index(i)) {
1184 1191 var cell = this.get_cell(i);
1185 1192 cell.set_heading_level(level);
1186 1193 this.set_dirty(true);
1187 1194 }
1188 1195 };
1189 1196
1190 1197
1191 1198 // Cut/Copy/Paste
1192 1199
1193 1200 /**
1194 1201 * Enable UI elements for pasting cells.
1195 1202 *
1196 1203 * @method enable_paste
1197 1204 */
1198 1205 Notebook.prototype.enable_paste = function () {
1199 1206 var that = this;
1200 1207 if (!this.paste_enabled) {
1201 1208 $('#paste_cell_replace').removeClass('disabled')
1202 1209 .on('click', function () {that.paste_cell_replace();});
1203 1210 $('#paste_cell_above').removeClass('disabled')
1204 1211 .on('click', function () {that.paste_cell_above();});
1205 1212 $('#paste_cell_below').removeClass('disabled')
1206 1213 .on('click', function () {that.paste_cell_below();});
1207 1214 this.paste_enabled = true;
1208 1215 }
1209 1216 };
1210 1217
1211 1218 /**
1212 1219 * Disable UI elements for pasting cells.
1213 1220 *
1214 1221 * @method disable_paste
1215 1222 */
1216 1223 Notebook.prototype.disable_paste = function () {
1217 1224 if (this.paste_enabled) {
1218 1225 $('#paste_cell_replace').addClass('disabled').off('click');
1219 1226 $('#paste_cell_above').addClass('disabled').off('click');
1220 1227 $('#paste_cell_below').addClass('disabled').off('click');
1221 1228 this.paste_enabled = false;
1222 1229 }
1223 1230 };
1224 1231
1225 1232 /**
1226 1233 * Cut a cell.
1227 1234 *
1228 1235 * @method cut_cell
1229 1236 */
1230 1237 Notebook.prototype.cut_cell = function () {
1231 1238 this.copy_cell();
1232 1239 this.delete_cell();
1233 1240 };
1234 1241
1235 1242 /**
1236 1243 * Copy a cell.
1237 1244 *
1238 1245 * @method copy_cell
1239 1246 */
1240 1247 Notebook.prototype.copy_cell = function () {
1241 1248 var cell = this.get_selected_cell();
1242 1249 this.clipboard = cell.toJSON();
1243 1250 // remove undeletable status from the copied cell
1244 1251 if (this.clipboard.metadata.deletable !== undefined) {
1245 1252 delete this.clipboard.metadata.deletable;
1246 1253 }
1247 1254 this.enable_paste();
1248 1255 };
1249 1256
1250 1257 /**
1251 1258 * Replace the selected cell with a cell in the clipboard.
1252 1259 *
1253 1260 * @method paste_cell_replace
1254 1261 */
1255 1262 Notebook.prototype.paste_cell_replace = function () {
1256 1263 if (this.clipboard !== null && this.paste_enabled) {
1257 1264 var cell_data = this.clipboard;
1258 1265 var new_cell = this.insert_cell_above(cell_data.cell_type);
1259 1266 new_cell.fromJSON(cell_data);
1260 1267 var old_cell = this.get_next_cell(new_cell);
1261 1268 this.delete_cell(this.find_cell_index(old_cell));
1262 1269 this.select(this.find_cell_index(new_cell));
1263 1270 }
1264 1271 };
1265 1272
1266 1273 /**
1267 1274 * Paste a cell from the clipboard above the selected cell.
1268 1275 *
1269 1276 * @method paste_cell_above
1270 1277 */
1271 1278 Notebook.prototype.paste_cell_above = function () {
1272 1279 if (this.clipboard !== null && this.paste_enabled) {
1273 1280 var cell_data = this.clipboard;
1274 1281 var new_cell = this.insert_cell_above(cell_data.cell_type);
1275 1282 new_cell.fromJSON(cell_data);
1276 1283 new_cell.focus_cell();
1277 1284 }
1278 1285 };
1279 1286
1280 1287 /**
1281 1288 * Paste a cell from the clipboard below the selected cell.
1282 1289 *
1283 1290 * @method paste_cell_below
1284 1291 */
1285 1292 Notebook.prototype.paste_cell_below = function () {
1286 1293 if (this.clipboard !== null && this.paste_enabled) {
1287 1294 var cell_data = this.clipboard;
1288 1295 var new_cell = this.insert_cell_below(cell_data.cell_type);
1289 1296 new_cell.fromJSON(cell_data);
1290 1297 new_cell.focus_cell();
1291 1298 }
1292 1299 };
1293 1300
1294 1301 // Split/merge
1295 1302
1296 1303 /**
1297 1304 * Split the selected cell into two, at the cursor.
1298 1305 *
1299 1306 * @method split_cell
1300 1307 */
1301 1308 Notebook.prototype.split_cell = function () {
1302 1309 var cell = this.get_selected_cell();
1303 1310 if (cell.is_splittable()) {
1304 1311 var texta = cell.get_pre_cursor();
1305 1312 var textb = cell.get_post_cursor();
1306 1313 cell.set_text(textb);
1307 1314 var new_cell = this.insert_cell_above(cell.cell_type);
1308 1315 // Unrender the new cell so we can call set_text.
1309 1316 new_cell.unrender();
1310 1317 new_cell.set_text(texta);
1311 1318 }
1312 1319 };
1313 1320
1314 1321 /**
1315 1322 * Combine the selected cell into the cell above it.
1316 1323 *
1317 1324 * @method merge_cell_above
1318 1325 */
1319 1326 Notebook.prototype.merge_cell_above = function () {
1320 1327 var index = this.get_selected_index();
1321 1328 var cell = this.get_cell(index);
1322 1329 var render = cell.rendered;
1323 1330 if (!cell.is_mergeable()) {
1324 1331 return;
1325 1332 }
1326 1333 if (index > 0) {
1327 1334 var upper_cell = this.get_cell(index-1);
1328 1335 if (!upper_cell.is_mergeable()) {
1329 1336 return;
1330 1337 }
1331 1338 var upper_text = upper_cell.get_text();
1332 1339 var text = cell.get_text();
1333 1340 if (cell instanceof codecell.CodeCell) {
1334 1341 cell.set_text(upper_text+'\n'+text);
1335 1342 } else {
1336 1343 cell.unrender(); // Must unrender before we set_text.
1337 1344 cell.set_text(upper_text+'\n\n'+text);
1338 1345 if (render) {
1339 1346 // The rendered state of the final cell should match
1340 1347 // that of the original selected cell;
1341 1348 cell.render();
1342 1349 }
1343 1350 }
1344 1351 this.delete_cell(index-1);
1345 1352 this.select(this.find_cell_index(cell));
1346 1353 }
1347 1354 };
1348 1355
1349 1356 /**
1350 1357 * Combine the selected cell into the cell below it.
1351 1358 *
1352 1359 * @method merge_cell_below
1353 1360 */
1354 1361 Notebook.prototype.merge_cell_below = function () {
1355 1362 var index = this.get_selected_index();
1356 1363 var cell = this.get_cell(index);
1357 1364 var render = cell.rendered;
1358 1365 if (!cell.is_mergeable()) {
1359 1366 return;
1360 1367 }
1361 1368 if (index < this.ncells()-1) {
1362 1369 var lower_cell = this.get_cell(index+1);
1363 1370 if (!lower_cell.is_mergeable()) {
1364 1371 return;
1365 1372 }
1366 1373 var lower_text = lower_cell.get_text();
1367 1374 var text = cell.get_text();
1368 1375 if (cell instanceof codecell.CodeCell) {
1369 1376 cell.set_text(text+'\n'+lower_text);
1370 1377 } else {
1371 1378 cell.unrender(); // Must unrender before we set_text.
1372 1379 cell.set_text(text+'\n\n'+lower_text);
1373 1380 if (render) {
1374 1381 // The rendered state of the final cell should match
1375 1382 // that of the original selected cell;
1376 1383 cell.render();
1377 1384 }
1378 1385 }
1379 1386 this.delete_cell(index+1);
1380 1387 this.select(this.find_cell_index(cell));
1381 1388 }
1382 1389 };
1383 1390
1384 1391
1385 1392 // Cell collapsing and output clearing
1386 1393
1387 1394 /**
1388 1395 * Hide a cell's output.
1389 1396 *
1390 1397 * @method collapse_output
1391 1398 * @param {Number} index A cell's numeric index
1392 1399 */
1393 1400 Notebook.prototype.collapse_output = function (index) {
1394 1401 var i = this.index_or_selected(index);
1395 1402 var cell = this.get_cell(i);
1396 1403 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1397 1404 cell.collapse_output();
1398 1405 this.set_dirty(true);
1399 1406 }
1400 1407 };
1401 1408
1402 1409 /**
1403 1410 * Hide each code cell's output area.
1404 1411 *
1405 1412 * @method collapse_all_output
1406 1413 */
1407 1414 Notebook.prototype.collapse_all_output = function () {
1408 1415 this.get_cells().map(function (cell, i) {
1409 1416 if (cell instanceof codecell.CodeCell) {
1410 1417 cell.collapse_output();
1411 1418 }
1412 1419 });
1413 1420 // this should not be set if the `collapse` key is removed from nbformat
1414 1421 this.set_dirty(true);
1415 1422 };
1416 1423
1417 1424 /**
1418 1425 * Show a cell's output.
1419 1426 *
1420 1427 * @method expand_output
1421 1428 * @param {Number} index A cell's numeric index
1422 1429 */
1423 1430 Notebook.prototype.expand_output = function (index) {
1424 1431 var i = this.index_or_selected(index);
1425 1432 var cell = this.get_cell(i);
1426 1433 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1427 1434 cell.expand_output();
1428 1435 this.set_dirty(true);
1429 1436 }
1430 1437 };
1431 1438
1432 1439 /**
1433 1440 * Expand each code cell's output area, and remove scrollbars.
1434 1441 *
1435 1442 * @method expand_all_output
1436 1443 */
1437 1444 Notebook.prototype.expand_all_output = function () {
1438 1445 this.get_cells().map(function (cell, i) {
1439 1446 if (cell instanceof codecell.CodeCell) {
1440 1447 cell.expand_output();
1441 1448 }
1442 1449 });
1443 1450 // this should not be set if the `collapse` key is removed from nbformat
1444 1451 this.set_dirty(true);
1445 1452 };
1446 1453
1447 1454 /**
1448 1455 * Clear the selected CodeCell's output area.
1449 1456 *
1450 1457 * @method clear_output
1451 1458 * @param {Number} index A cell's numeric index
1452 1459 */
1453 1460 Notebook.prototype.clear_output = function (index) {
1454 1461 var i = this.index_or_selected(index);
1455 1462 var cell = this.get_cell(i);
1456 1463 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1457 1464 cell.clear_output();
1458 1465 this.set_dirty(true);
1459 1466 }
1460 1467 };
1461 1468
1462 1469 /**
1463 1470 * Clear each code cell's output area.
1464 1471 *
1465 1472 * @method clear_all_output
1466 1473 */
1467 1474 Notebook.prototype.clear_all_output = function () {
1468 1475 this.get_cells().map(function (cell, i) {
1469 1476 if (cell instanceof codecell.CodeCell) {
1470 1477 cell.clear_output();
1471 1478 }
1472 1479 });
1473 1480 this.set_dirty(true);
1474 1481 };
1475 1482
1476 1483 /**
1477 1484 * Scroll the selected CodeCell's output area.
1478 1485 *
1479 1486 * @method scroll_output
1480 1487 * @param {Number} index A cell's numeric index
1481 1488 */
1482 1489 Notebook.prototype.scroll_output = function (index) {
1483 1490 var i = this.index_or_selected(index);
1484 1491 var cell = this.get_cell(i);
1485 1492 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1486 1493 cell.scroll_output();
1487 1494 this.set_dirty(true);
1488 1495 }
1489 1496 };
1490 1497
1491 1498 /**
1492 1499 * Expand each code cell's output area, and add a scrollbar for long output.
1493 1500 *
1494 1501 * @method scroll_all_output
1495 1502 */
1496 1503 Notebook.prototype.scroll_all_output = function () {
1497 1504 this.get_cells().map(function (cell, i) {
1498 1505 if (cell instanceof codecell.CodeCell) {
1499 1506 cell.scroll_output();
1500 1507 }
1501 1508 });
1502 1509 // this should not be set if the `collapse` key is removed from nbformat
1503 1510 this.set_dirty(true);
1504 1511 };
1505 1512
1506 1513 /** Toggle whether a cell's output is collapsed or expanded.
1507 1514 *
1508 1515 * @method toggle_output
1509 1516 * @param {Number} index A cell's numeric index
1510 1517 */
1511 1518 Notebook.prototype.toggle_output = function (index) {
1512 1519 var i = this.index_or_selected(index);
1513 1520 var cell = this.get_cell(i);
1514 1521 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1515 1522 cell.toggle_output();
1516 1523 this.set_dirty(true);
1517 1524 }
1518 1525 };
1519 1526
1520 1527 /**
1521 1528 * Hide/show the output of all cells.
1522 1529 *
1523 1530 * @method toggle_all_output
1524 1531 */
1525 1532 Notebook.prototype.toggle_all_output = function () {
1526 1533 this.get_cells().map(function (cell, i) {
1527 1534 if (cell instanceof codecell.CodeCell) {
1528 1535 cell.toggle_output();
1529 1536 }
1530 1537 });
1531 1538 // this should not be set if the `collapse` key is removed from nbformat
1532 1539 this.set_dirty(true);
1533 1540 };
1534 1541
1535 1542 /**
1536 1543 * Toggle a scrollbar for long cell outputs.
1537 1544 *
1538 1545 * @method toggle_output_scroll
1539 1546 * @param {Number} index A cell's numeric index
1540 1547 */
1541 1548 Notebook.prototype.toggle_output_scroll = function (index) {
1542 1549 var i = this.index_or_selected(index);
1543 1550 var cell = this.get_cell(i);
1544 1551 if (cell !== null && (cell instanceof codecell.CodeCell)) {
1545 1552 cell.toggle_output_scroll();
1546 1553 this.set_dirty(true);
1547 1554 }
1548 1555 };
1549 1556
1550 1557 /**
1551 1558 * Toggle the scrolling of long output on all cells.
1552 1559 *
1553 1560 * @method toggle_all_output_scrolling
1554 1561 */
1555 1562 Notebook.prototype.toggle_all_output_scroll = function () {
1556 1563 this.get_cells().map(function (cell, i) {
1557 1564 if (cell instanceof codecell.CodeCell) {
1558 1565 cell.toggle_output_scroll();
1559 1566 }
1560 1567 });
1561 1568 // this should not be set if the `collapse` key is removed from nbformat
1562 1569 this.set_dirty(true);
1563 1570 };
1564 1571
1565 1572 // Other cell functions: line numbers, ...
1566 1573
1567 1574 /**
1568 1575 * Toggle line numbers in the selected cell's input area.
1569 1576 *
1570 1577 * @method cell_toggle_line_numbers
1571 1578 */
1572 1579 Notebook.prototype.cell_toggle_line_numbers = function() {
1573 1580 this.get_selected_cell().toggle_line_numbers();
1574 1581 };
1575 1582
1576 1583 /**
1577 1584 * Set the codemirror mode for all code cells, including the default for
1578 1585 * new code cells.
1579 1586 *
1580 1587 * @method set_codemirror_mode
1581 1588 */
1582 1589 Notebook.prototype.set_codemirror_mode = function(newmode){
1583 1590 if (newmode === this.codemirror_mode) {
1584 1591 return;
1585 1592 }
1586 1593 this.codemirror_mode = newmode;
1587 1594 codecell.CodeCell.options_default.cm_config.mode = newmode;
1588 1595
1589 1596 var that = this;
1590 1597 utils.requireCodeMirrorMode(newmode, function (spec) {
1591 1598 that.get_cells().map(function(cell, i) {
1592 1599 if (cell.cell_type === 'code'){
1593 1600 cell.code_mirror.setOption('mode', spec);
1594 1601 // This is currently redundant, because cm_config ends up as
1595 1602 // codemirror's own .options object, but I don't want to
1596 1603 // rely on that.
1597 1604 cell.cm_config.mode = spec;
1598 1605 }
1599 1606 });
1600 1607 });
1601 1608 };
1602 1609
1603 1610 // Session related things
1604 1611
1605 1612 /**
1606 1613 * Start a new session and set it on each code cell.
1607 1614 *
1608 1615 * @method start_session
1609 1616 */
1610 1617 Notebook.prototype.start_session = function (kernel_name) {
1611 1618 if (this._session_starting) {
1612 1619 throw new session.SessionAlreadyStarting();
1613 1620 }
1614 1621 this._session_starting = true;
1615 1622
1616 1623 var options = {
1617 1624 base_url: this.base_url,
1618 1625 ws_url: this.ws_url,
1619 1626 notebook_path: this.notebook_path,
1620 1627 notebook_name: this.notebook_name,
1621 1628 kernel_name: kernel_name,
1622 1629 notebook: this
1623 1630 };
1624 1631
1625 1632 var success = $.proxy(this._session_started, this);
1626 1633 var failure = $.proxy(this._session_start_failed, this);
1627 1634
1628 1635 if (this.session !== null) {
1629 1636 this.session.restart(options, success, failure);
1630 1637 } else {
1631 1638 this.session = new session.Session(options);
1632 1639 this.session.start(success, failure);
1633 1640 }
1634 1641 };
1635 1642
1636 1643
1637 1644 /**
1638 1645 * Once a session is started, link the code cells to the kernel and pass the
1639 1646 * comm manager to the widget manager
1640 1647 *
1641 1648 */
1642 1649 Notebook.prototype._session_started = function (){
1643 1650 this._session_starting = false;
1644 1651 this.kernel = this.session.kernel;
1645 1652 var ncells = this.ncells();
1646 1653 for (var i=0; i<ncells; i++) {
1647 1654 var cell = this.get_cell(i);
1648 1655 if (cell instanceof codecell.CodeCell) {
1649 1656 cell.set_kernel(this.session.kernel);
1650 1657 }
1651 1658 }
1652 1659 };
1653 1660 Notebook.prototype._session_start_failed = function (jqxhr, status, error){
1654 1661 this._session_starting = false;
1655 1662 utils.log_ajax_error(jqxhr, status, error);
1656 1663 };
1657 1664
1658 1665 /**
1659 1666 * Prompt the user to restart the IPython kernel.
1660 1667 *
1661 1668 * @method restart_kernel
1662 1669 */
1663 1670 Notebook.prototype.restart_kernel = function () {
1664 1671 var that = this;
1665 1672 dialog.modal({
1666 1673 notebook: this,
1667 1674 keyboard_manager: this.keyboard_manager,
1668 1675 title : "Restart kernel or continue running?",
1669 1676 body : $("<p/>").text(
1670 1677 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1671 1678 ),
1672 1679 buttons : {
1673 1680 "Continue running" : {},
1674 1681 "Restart" : {
1675 1682 "class" : "btn-danger",
1676 1683 "click" : function() {
1677 1684 that.kernel.restart();
1678 1685 }
1679 1686 }
1680 1687 }
1681 1688 });
1682 1689 };
1683 1690
1684 1691 /**
1685 1692 * Execute or render cell outputs and go into command mode.
1686 1693 *
1687 1694 * @method execute_cell
1688 1695 */
1689 1696 Notebook.prototype.execute_cell = function () {
1690 1697 /**
1691 1698 * mode = shift, ctrl, alt
1692 1699 */
1693 1700 var cell = this.get_selected_cell();
1694 1701
1695 1702 cell.execute();
1696 1703 this.command_mode();
1697 1704 this.set_dirty(true);
1698 1705 };
1699 1706
1700 1707 /**
1701 1708 * Execute or render cell outputs and insert a new cell below.
1702 1709 *
1703 1710 * @method execute_cell_and_insert_below
1704 1711 */
1705 1712 Notebook.prototype.execute_cell_and_insert_below = function () {
1706 1713 var cell = this.get_selected_cell();
1707 1714 var cell_index = this.find_cell_index(cell);
1708 1715
1709 1716 cell.execute();
1710 1717
1711 1718 // If we are at the end always insert a new cell and return
1712 1719 if (cell_index === (this.ncells()-1)) {
1713 1720 this.command_mode();
1714 1721 this.insert_cell_below();
1715 1722 this.select(cell_index+1);
1716 1723 this.edit_mode();
1717 1724 this.scroll_to_bottom();
1718 1725 this.set_dirty(true);
1719 1726 return;
1720 1727 }
1721 1728
1722 1729 this.command_mode();
1723 1730 this.insert_cell_below();
1724 1731 this.select(cell_index+1);
1725 1732 this.edit_mode();
1726 1733 this.set_dirty(true);
1727 1734 };
1728 1735
1729 1736 /**
1730 1737 * Execute or render cell outputs and select the next cell.
1731 1738 *
1732 1739 * @method execute_cell_and_select_below
1733 1740 */
1734 1741 Notebook.prototype.execute_cell_and_select_below = function () {
1735 1742
1736 1743 var cell = this.get_selected_cell();
1737 1744 var cell_index = this.find_cell_index(cell);
1738 1745
1739 1746 cell.execute();
1740 1747
1741 1748 // If we are at the end always insert a new cell and return
1742 1749 if (cell_index === (this.ncells()-1)) {
1743 1750 this.command_mode();
1744 1751 this.insert_cell_below();
1745 1752 this.select(cell_index+1);
1746 1753 this.edit_mode();
1747 1754 this.scroll_to_bottom();
1748 1755 this.set_dirty(true);
1749 1756 return;
1750 1757 }
1751 1758
1752 1759 this.command_mode();
1753 1760 this.select(cell_index+1);
1754 1761 this.focus_cell();
1755 1762 this.set_dirty(true);
1756 1763 };
1757 1764
1758 1765 /**
1759 1766 * Execute all cells below the selected cell.
1760 1767 *
1761 1768 * @method execute_cells_below
1762 1769 */
1763 1770 Notebook.prototype.execute_cells_below = function () {
1764 1771 this.execute_cell_range(this.get_selected_index(), this.ncells());
1765 1772 this.scroll_to_bottom();
1766 1773 };
1767 1774
1768 1775 /**
1769 1776 * Execute all cells above the selected cell.
1770 1777 *
1771 1778 * @method execute_cells_above
1772 1779 */
1773 1780 Notebook.prototype.execute_cells_above = function () {
1774 1781 this.execute_cell_range(0, this.get_selected_index());
1775 1782 };
1776 1783
1777 1784 /**
1778 1785 * Execute all cells.
1779 1786 *
1780 1787 * @method execute_all_cells
1781 1788 */
1782 1789 Notebook.prototype.execute_all_cells = function () {
1783 1790 this.execute_cell_range(0, this.ncells());
1784 1791 this.scroll_to_bottom();
1785 1792 };
1786 1793
1787 1794 /**
1788 1795 * Execute a contiguous range of cells.
1789 1796 *
1790 1797 * @method execute_cell_range
1791 1798 * @param {Number} start Index of the first cell to execute (inclusive)
1792 1799 * @param {Number} end Index of the last cell to execute (exclusive)
1793 1800 */
1794 1801 Notebook.prototype.execute_cell_range = function (start, end) {
1795 1802 this.command_mode();
1796 1803 for (var i=start; i<end; i++) {
1797 1804 this.select(i);
1798 1805 this.execute_cell();
1799 1806 }
1800 1807 };
1801 1808
1802 1809 // Persistance and loading
1803 1810
1804 1811 /**
1805 1812 * Getter method for this notebook's name.
1806 1813 *
1807 1814 * @method get_notebook_name
1808 1815 * @return {String} This notebook's name (excluding file extension)
1809 1816 */
1810 1817 Notebook.prototype.get_notebook_name = function () {
1811 1818 var nbname = this.notebook_name.substring(0,this.notebook_name.length-6);
1812 1819 return nbname;
1813 1820 };
1814 1821
1815 1822 /**
1816 1823 * Setter method for this notebook's name.
1817 1824 *
1818 1825 * @method set_notebook_name
1819 1826 * @param {String} name A new name for this notebook
1820 1827 */
1821 1828 Notebook.prototype.set_notebook_name = function (name) {
1822 1829 var parent = utils.url_path_split(this.notebook_path)[0];
1823 1830 this.notebook_name = name;
1824 1831 this.notebook_path = utils.url_path_join(parent, name);
1825 1832 };
1826 1833
1827 1834 /**
1828 1835 * Check that a notebook's name is valid.
1829 1836 *
1830 1837 * @method test_notebook_name
1831 1838 * @param {String} nbname A name for this notebook
1832 1839 * @return {Boolean} True if the name is valid, false if invalid
1833 1840 */
1834 1841 Notebook.prototype.test_notebook_name = function (nbname) {
1835 1842 nbname = nbname || '';
1836 1843 if (nbname.length>0 && !this.notebook_name_blacklist_re.test(nbname)) {
1837 1844 return true;
1838 1845 } else {
1839 1846 return false;
1840 1847 }
1841 1848 };
1842 1849
1843 1850 /**
1844 1851 * Load a notebook from JSON (.ipynb).
1845 1852 *
1846 1853 * @method fromJSON
1847 1854 * @param {Object} data JSON representation of a notebook
1848 1855 */
1849 1856 Notebook.prototype.fromJSON = function (data) {
1850 1857
1851 1858 var content = data.content;
1852 1859 var ncells = this.ncells();
1853 1860 var i;
1854 1861 for (i=0; i<ncells; i++) {
1855 1862 // Always delete cell 0 as they get renumbered as they are deleted.
1856 1863 this.delete_cell(0);
1857 1864 }
1858 1865 // Save the metadata and name.
1859 1866 this.metadata = content.metadata;
1860 1867 this.notebook_name = data.name;
1861 1868 this.notebook_path = data.path;
1862 1869 var trusted = true;
1863 1870
1864 1871 // Trigger an event changing the kernel spec - this will set the default
1865 1872 // codemirror mode
1866 1873 if (this.metadata.kernelspec !== undefined) {
1867 1874 this.events.trigger('spec_changed.Kernel', this.metadata.kernelspec);
1868 1875 }
1869 1876
1870 1877 // Set the codemirror mode from language_info metadata
1871 1878 if (this.metadata.language_info !== undefined) {
1872 1879 var langinfo = this.metadata.language_info;
1873 1880 // Mode 'null' should be plain, unhighlighted text.
1874 1881 var cm_mode = langinfo.codemirror_mode || langinfo.name || 'null';
1875 1882 this.set_codemirror_mode(cm_mode);
1876 1883 }
1877 1884
1878 1885 var new_cells = content.cells;
1879 1886 ncells = new_cells.length;
1880 1887 var cell_data = null;
1881 1888 var new_cell = null;
1882 1889 for (i=0; i<ncells; i++) {
1883 1890 cell_data = new_cells[i];
1884 1891 new_cell = this.insert_cell_at_index(cell_data.cell_type, i);
1885 1892 new_cell.fromJSON(cell_data);
1886 1893 if (new_cell.cell_type == 'code' && !new_cell.output_area.trusted) {
1887 1894 trusted = false;
1888 1895 }
1889 1896 }
1890 1897 if (trusted !== this.trusted) {
1891 1898 this.trusted = trusted;
1892 1899 this.events.trigger("trust_changed.Notebook", trusted);
1893 1900 }
1894 1901 };
1895 1902
1896 1903 /**
1897 1904 * Dump this notebook into a JSON-friendly object.
1898 1905 *
1899 1906 * @method toJSON
1900 1907 * @return {Object} A JSON-friendly representation of this notebook.
1901 1908 */
1902 1909 Notebook.prototype.toJSON = function () {
1903 1910 /**
1904 1911 * remove the conversion indicator, which only belongs in-memory
1905 1912 */
1906 1913 delete this.metadata.orig_nbformat;
1907 1914 delete this.metadata.orig_nbformat_minor;
1908 1915
1909 1916 var cells = this.get_cells();
1910 1917 var ncells = cells.length;
1911 1918 var cell_array = new Array(ncells);
1912 1919 var trusted = true;
1913 1920 for (var i=0; i<ncells; i++) {
1914 1921 var cell = cells[i];
1915 1922 if (cell.cell_type == 'code' && !cell.output_area.trusted) {
1916 1923 trusted = false;
1917 1924 }
1918 1925 cell_array[i] = cell.toJSON();
1919 1926 }
1920 1927 var data = {
1921 1928 cells: cell_array,
1922 1929 metadata: this.metadata,
1923 1930 nbformat: this.nbformat,
1924 1931 nbformat_minor: this.nbformat_minor
1925 1932 };
1926 1933 if (trusted != this.trusted) {
1927 1934 this.trusted = trusted;
1928 1935 this.events.trigger("trust_changed.Notebook", trusted);
1929 1936 }
1930 1937 return data;
1931 1938 };
1932 1939
1933 1940 /**
1934 1941 * Start an autosave timer, for periodically saving the notebook.
1935 1942 *
1936 1943 * @method set_autosave_interval
1937 1944 * @param {Integer} interval the autosave interval in milliseconds
1938 1945 */
1939 1946 Notebook.prototype.set_autosave_interval = function (interval) {
1940 1947 var that = this;
1941 1948 // clear previous interval, so we don't get simultaneous timers
1942 1949 if (this.autosave_timer) {
1943 1950 clearInterval(this.autosave_timer);
1944 1951 }
1945 1952 if (!this.writable) {
1946 1953 // disable autosave if not writable
1947 1954 interval = 0;
1948 1955 }
1949 1956
1950 1957 this.autosave_interval = this.minimum_autosave_interval = interval;
1951 1958 if (interval) {
1952 1959 this.autosave_timer = setInterval(function() {
1953 1960 if (that.dirty) {
1954 1961 that.save_notebook();
1955 1962 }
1956 1963 }, interval);
1957 1964 this.events.trigger("autosave_enabled.Notebook", interval);
1958 1965 } else {
1959 1966 this.autosave_timer = null;
1960 1967 this.events.trigger("autosave_disabled.Notebook");
1961 1968 }
1962 1969 };
1963 1970
1964 1971 /**
1965 1972 * Save this notebook on the server. This becomes a notebook instance's
1966 1973 * .save_notebook method *after* the entire notebook has been loaded.
1967 1974 *
1968 1975 * @method save_notebook
1969 1976 */
1970 1977 Notebook.prototype.save_notebook = function () {
1971 1978 if (!this._fully_loaded) {
1972 1979 this.events.trigger('notebook_save_failed.Notebook',
1973 1980 new Error("Load failed, save is disabled")
1974 1981 );
1975 1982 return;
1976 1983 } else if (!this.writable) {
1977 1984 this.events.trigger('notebook_save_failed.Notebook',
1978 1985 new Error("Notebook is read-only")
1979 1986 );
1980 1987 return;
1981 1988 }
1982 1989
1983 1990 // Create a JSON model to be sent to the server.
1984 1991 var model = {
1985 1992 type : "notebook",
1986 1993 content : this.toJSON()
1987 1994 };
1988 1995 // time the ajax call for autosave tuning purposes.
1989 1996 var start = new Date().getTime();
1990 1997
1991 1998 var that = this;
1992 1999 return this.contents.save(this.notebook_path, model).then(
1993 2000 $.proxy(this.save_notebook_success, this, start),
1994 2001 function (error) {
1995 2002 that.events.trigger('notebook_save_failed.Notebook', error);
1996 2003 }
1997 2004 );
1998 2005 };
1999 2006
2000 2007 /**
2001 2008 * Success callback for saving a notebook.
2002 2009 *
2003 2010 * @method save_notebook_success
2004 2011 * @param {Integer} start Time when the save request start
2005 2012 * @param {Object} data JSON representation of a notebook
2006 2013 */
2007 2014 Notebook.prototype.save_notebook_success = function (start, data) {
2008 2015 this.set_dirty(false);
2009 2016 if (data.message) {
2010 2017 // save succeeded, but validation failed.
2011 2018 var body = $("<div>");
2012 2019 var title = "Notebook validation failed";
2013 2020
2014 2021 body.append($("<p>").text(
2015 2022 "The save operation succeeded," +
2016 2023 " but the notebook does not appear to be valid." +
2017 2024 " The validation error was:"
2018 2025 )).append($("<div>").addClass("validation-error").append(
2019 2026 $("<pre>").text(data.message)
2020 2027 ));
2021 2028 dialog.modal({
2022 2029 notebook: this,
2023 2030 keyboard_manager: this.keyboard_manager,
2024 2031 title: title,
2025 2032 body: body,
2026 2033 buttons : {
2027 2034 OK : {
2028 2035 "class" : "btn-primary"
2029 2036 }
2030 2037 }
2031 2038 });
2032 2039 }
2033 2040 this.events.trigger('notebook_saved.Notebook');
2034 2041 this._update_autosave_interval(start);
2035 2042 if (this._checkpoint_after_save) {
2036 2043 this.create_checkpoint();
2037 2044 this._checkpoint_after_save = false;
2038 2045 }
2039 2046 };
2040 2047
2041 2048 /**
2042 2049 * update the autosave interval based on how long the last save took
2043 2050 *
2044 2051 * @method _update_autosave_interval
2045 2052 * @param {Integer} timestamp when the save request started
2046 2053 */
2047 2054 Notebook.prototype._update_autosave_interval = function (start) {
2048 2055 var duration = (new Date().getTime() - start);
2049 2056 if (this.autosave_interval) {
2050 2057 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
2051 2058 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
2052 2059 // round to 10 seconds, otherwise we will be setting a new interval too often
2053 2060 interval = 10000 * Math.round(interval / 10000);
2054 2061 // set new interval, if it's changed
2055 2062 if (interval != this.autosave_interval) {
2056 2063 this.set_autosave_interval(interval);
2057 2064 }
2058 2065 }
2059 2066 };
2060 2067
2061 2068 /**
2062 2069 * Explicitly trust the output of this notebook.
2063 2070 *
2064 2071 * @method trust_notebook
2065 2072 */
2066 2073 Notebook.prototype.trust_notebook = function () {
2067 2074 var body = $("<div>").append($("<p>")
2068 2075 .text("A trusted IPython notebook may execute hidden malicious code ")
2069 2076 .append($("<strong>")
2070 2077 .append(
2071 2078 $("<em>").text("when you open it")
2072 2079 )
2073 2080 ).append(".").append(
2074 2081 " Selecting trust will immediately reload this notebook in a trusted state."
2075 2082 ).append(
2076 2083 " For more information, see the "
2077 2084 ).append($("<a>").attr("href", "http://ipython.org/ipython-doc/2/notebook/security.html")
2078 2085 .text("IPython security documentation")
2079 2086 ).append(".")
2080 2087 );
2081 2088
2082 2089 var nb = this;
2083 2090 dialog.modal({
2084 2091 notebook: this,
2085 2092 keyboard_manager: this.keyboard_manager,
2086 2093 title: "Trust this notebook?",
2087 2094 body: body,
2088 2095
2089 2096 buttons: {
2090 2097 Cancel : {},
2091 2098 Trust : {
2092 2099 class : "btn-danger",
2093 2100 click : function () {
2094 2101 var cells = nb.get_cells();
2095 2102 for (var i = 0; i < cells.length; i++) {
2096 2103 var cell = cells[i];
2097 2104 if (cell.cell_type == 'code') {
2098 2105 cell.output_area.trusted = true;
2099 2106 }
2100 2107 }
2101 2108 nb.events.on('notebook_saved.Notebook', function () {
2102 2109 window.location.reload();
2103 2110 });
2104 2111 nb.save_notebook();
2105 2112 }
2106 2113 }
2107 2114 }
2108 2115 });
2109 2116 };
2110 2117
2111 2118 Notebook.prototype.copy_notebook = function () {
2112 2119 var that = this;
2113 2120 var base_url = this.base_url;
2114 2121 var w = window.open();
2115 2122 var parent = utils.url_path_split(this.notebook_path)[0];
2116 2123 this.contents.copy(this.notebook_path, parent).then(
2117 2124 function (data) {
2118 2125 w.location = utils.url_join_encode(
2119 2126 base_url, 'notebooks', data.path
2120 2127 );
2121 2128 },
2122 2129 function(error) {
2123 2130 w.close();
2124 2131 that.events.trigger('notebook_copy_failed', error);
2125 2132 }
2126 2133 );
2127 2134 };
2128 2135
2129 2136 Notebook.prototype.rename = function (new_name) {
2130 2137 if (!new_name.match(/\.ipynb$/)) {
2131 2138 new_name = new_name + ".ipynb";
2132 2139 }
2133 2140
2134 2141 var that = this;
2135 2142 var parent = utils.url_path_split(this.notebook_path)[0];
2136 2143 var new_path = utils.url_path_join(parent, new_name);
2137 2144 return this.contents.rename(this.notebook_path, new_path).then(
2138 2145 function (json) {
2139 2146 that.notebook_name = json.name;
2140 2147 that.notebook_path = json.path;
2141 2148 that.session.rename_notebook(json.path);
2142 2149 that.events.trigger('notebook_renamed.Notebook', json);
2143 2150 }
2144 2151 );
2145 2152 };
2146 2153
2147 2154 Notebook.prototype.delete = function () {
2148 2155 this.contents.delete(this.notebook_path);
2149 2156 };
2150 2157
2151 2158 /**
2152 2159 * Request a notebook's data from the server.
2153 2160 *
2154 2161 * @method load_notebook
2155 2162 * @param {String} notebook_path A notebook to load
2156 2163 */
2157 2164 Notebook.prototype.load_notebook = function (notebook_path) {
2158 2165 this.notebook_path = notebook_path;
2159 2166 this.notebook_name = utils.url_path_split(this.notebook_path)[1];
2160 2167 this.events.trigger('notebook_loading.Notebook');
2161 2168 this.contents.get(notebook_path, {type: 'notebook'}).then(
2162 2169 $.proxy(this.load_notebook_success, this),
2163 2170 $.proxy(this.load_notebook_error, this)
2164 2171 );
2165 2172 };
2166 2173
2167 2174 /**
2168 2175 * Success callback for loading a notebook from the server.
2169 2176 *
2170 2177 * Load notebook data from the JSON response.
2171 2178 *
2172 2179 * @method load_notebook_success
2173 2180 * @param {Object} data JSON representation of a notebook
2174 2181 */
2175 2182 Notebook.prototype.load_notebook_success = function (data) {
2176 2183 var failed, msg;
2177 2184 try {
2178 2185 this.fromJSON(data);
2179 2186 } catch (e) {
2180 2187 failed = e;
2181 2188 console.log("Notebook failed to load from JSON:", e);
2182 2189 }
2183 2190 if (failed || data.message) {
2184 2191 // *either* fromJSON failed or validation failed
2185 2192 var body = $("<div>");
2186 2193 var title;
2187 2194 if (failed) {
2188 2195 title = "Notebook failed to load";
2189 2196 body.append($("<p>").text(
2190 2197 "The error was: "
2191 2198 )).append($("<div>").addClass("js-error").text(
2192 2199 failed.toString()
2193 2200 )).append($("<p>").text(
2194 2201 "See the error console for details."
2195 2202 ));
2196 2203 } else {
2197 2204 title = "Notebook validation failed";
2198 2205 }
2199 2206
2200 2207 if (data.message) {
2201 2208 if (failed) {
2202 2209 msg = "The notebook also failed validation:";
2203 2210 } else {
2204 2211 msg = "An invalid notebook may not function properly." +
2205 2212 " The validation error was:";
2206 2213 }
2207 2214 body.append($("<p>").text(
2208 2215 msg
2209 2216 )).append($("<div>").addClass("validation-error").append(
2210 2217 $("<pre>").text(data.message)
2211 2218 ));
2212 2219 }
2213 2220
2214 2221 dialog.modal({
2215 2222 notebook: this,
2216 2223 keyboard_manager: this.keyboard_manager,
2217 2224 title: title,
2218 2225 body: body,
2219 2226 buttons : {
2220 2227 OK : {
2221 2228 "class" : "btn-primary"
2222 2229 }
2223 2230 }
2224 2231 });
2225 2232 }
2226 2233 if (this.ncells() === 0) {
2227 2234 this.insert_cell_below('code');
2228 2235 this.edit_mode(0);
2229 2236 } else {
2230 2237 this.select(0);
2231 2238 this.handle_command_mode(this.get_cell(0));
2232 2239 }
2233 2240 this.set_dirty(false);
2234 2241 this.scroll_to_top();
2235 2242 this.writable = data.writable || false;
2236 2243 var nbmodel = data.content;
2237 2244 var orig_nbformat = nbmodel.metadata.orig_nbformat;
2238 2245 var orig_nbformat_minor = nbmodel.metadata.orig_nbformat_minor;
2239 2246 if (orig_nbformat !== undefined && nbmodel.nbformat !== orig_nbformat) {
2240 2247 var src;
2241 2248 if (nbmodel.nbformat > orig_nbformat) {
2242 2249 src = " an older notebook format ";
2243 2250 } else {
2244 2251 src = " a newer notebook format ";
2245 2252 }
2246 2253
2247 2254 msg = "This notebook has been converted from" + src +
2248 2255 "(v"+orig_nbformat+") to the current notebook " +
2249 2256 "format (v"+nbmodel.nbformat+"). The next time you save this notebook, the " +
2250 2257 "current notebook format will be used.";
2251 2258
2252 2259 if (nbmodel.nbformat > orig_nbformat) {
2253 2260 msg += " Older versions of IPython may not be able to read the new format.";
2254 2261 } else {
2255 2262 msg += " Some features of the original notebook may not be available.";
2256 2263 }
2257 2264 msg += " To preserve the original version, close the " +
2258 2265 "notebook without saving it.";
2259 2266 dialog.modal({
2260 2267 notebook: this,
2261 2268 keyboard_manager: this.keyboard_manager,
2262 2269 title : "Notebook converted",
2263 2270 body : msg,
2264 2271 buttons : {
2265 2272 OK : {
2266 2273 class : "btn-primary"
2267 2274 }
2268 2275 }
2269 2276 });
2270 2277 } else if (this.nbformat_minor < nbmodel.nbformat_minor) {
2271 2278 this.nbformat_minor = nbmodel.nbformat_minor;
2272 2279 }
2273 2280
2274 2281 // Create the session after the notebook is completely loaded to prevent
2275 2282 // code execution upon loading, which is a security risk.
2276 2283 if (this.session === null) {
2277 2284 var kernel_name;
2278 2285 if (this.metadata.kernelspec) {
2279 2286 var kernelspec = this.metadata.kernelspec || {};
2280 2287 kernel_name = kernelspec.name;
2281 2288 } else {
2282 2289 kernel_name = utils.get_url_param('kernel_name');
2283 2290 }
2284 2291 this.start_session(kernel_name);
2285 2292 }
2286 2293 // load our checkpoint list
2287 2294 this.list_checkpoints();
2288 2295
2289 2296 // load toolbar state
2290 2297 if (this.metadata.celltoolbar) {
2291 2298 celltoolbar.CellToolbar.global_show();
2292 2299 celltoolbar.CellToolbar.activate_preset(this.metadata.celltoolbar);
2293 2300 } else {
2294 2301 celltoolbar.CellToolbar.global_hide();
2295 2302 }
2296 2303
2297 2304 if (!this.writable) {
2298 2305 this.set_autosave_interval(0);
2299 2306 this.events.trigger('notebook_read_only.Notebook');
2300 2307 }
2301 2308
2302 2309 // now that we're fully loaded, it is safe to restore save functionality
2303 2310 this._fully_loaded = true;
2304 2311 this.events.trigger('notebook_loaded.Notebook');
2305 2312 };
2306 2313
2307 2314 /**
2308 2315 * Failure callback for loading a notebook from the server.
2309 2316 *
2310 2317 * @method load_notebook_error
2311 2318 * @param {Error} error
2312 2319 */
2313 2320 Notebook.prototype.load_notebook_error = function (error) {
2314 2321 this.events.trigger('notebook_load_failed.Notebook', error);
2315 2322 var msg;
2316 2323 if (error.name === utils.XHR_ERROR && error.xhr.status === 500) {
2317 2324 utils.log_ajax_error(error.xhr, error.xhr_status, error.xhr_error);
2318 2325 msg = "An unknown error occurred while loading this notebook. " +
2319 2326 "This version can load notebook formats " +
2320 2327 "v" + this.nbformat + " or earlier. See the server log for details.";
2321 2328 } else {
2322 2329 msg = error.message;
2323 2330 }
2324 2331 dialog.modal({
2325 2332 notebook: this,
2326 2333 keyboard_manager: this.keyboard_manager,
2327 2334 title: "Error loading notebook",
2328 2335 body : msg,
2329 2336 buttons : {
2330 2337 "OK": {}
2331 2338 }
2332 2339 });
2333 2340 };
2334 2341
2335 2342 /********************* checkpoint-related *********************/
2336 2343
2337 2344 /**
2338 2345 * Save the notebook then immediately create a checkpoint.
2339 2346 *
2340 2347 * @method save_checkpoint
2341 2348 */
2342 2349 Notebook.prototype.save_checkpoint = function () {
2343 2350 this._checkpoint_after_save = true;
2344 2351 this.save_notebook();
2345 2352 };
2346 2353
2347 2354 /**
2348 2355 * Add a checkpoint for this notebook.
2349 2356 * for use as a callback from checkpoint creation.
2350 2357 *
2351 2358 * @method add_checkpoint
2352 2359 */
2353 2360 Notebook.prototype.add_checkpoint = function (checkpoint) {
2354 2361 var found = false;
2355 2362 for (var i = 0; i < this.checkpoints.length; i++) {
2356 2363 var existing = this.checkpoints[i];
2357 2364 if (existing.id == checkpoint.id) {
2358 2365 found = true;
2359 2366 this.checkpoints[i] = checkpoint;
2360 2367 break;
2361 2368 }
2362 2369 }
2363 2370 if (!found) {
2364 2371 this.checkpoints.push(checkpoint);
2365 2372 }
2366 2373 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
2367 2374 };
2368 2375
2369 2376 /**
2370 2377 * List checkpoints for this notebook.
2371 2378 *
2372 2379 * @method list_checkpoints
2373 2380 */
2374 2381 Notebook.prototype.list_checkpoints = function () {
2375 2382 var that = this;
2376 2383 this.contents.list_checkpoints(this.notebook_path).then(
2377 2384 $.proxy(this.list_checkpoints_success, this),
2378 2385 function(error) {
2379 2386 that.events.trigger('list_checkpoints_failed.Notebook', error);
2380 2387 }
2381 2388 );
2382 2389 };
2383 2390
2384 2391 /**
2385 2392 * Success callback for listing checkpoints.
2386 2393 *
2387 2394 * @method list_checkpoint_success
2388 2395 * @param {Object} data JSON representation of a checkpoint
2389 2396 */
2390 2397 Notebook.prototype.list_checkpoints_success = function (data) {
2391 2398 this.checkpoints = data;
2392 2399 if (data.length) {
2393 2400 this.last_checkpoint = data[data.length - 1];
2394 2401 } else {
2395 2402 this.last_checkpoint = null;
2396 2403 }
2397 2404 this.events.trigger('checkpoints_listed.Notebook', [data]);
2398 2405 };
2399 2406
2400 2407 /**
2401 2408 * Create a checkpoint of this notebook on the server from the most recent save.
2402 2409 *
2403 2410 * @method create_checkpoint
2404 2411 */
2405 2412 Notebook.prototype.create_checkpoint = function () {
2406 2413 var that = this;
2407 2414 this.contents.create_checkpoint(this.notebook_path).then(
2408 2415 $.proxy(this.create_checkpoint_success, this),
2409 2416 function (error) {
2410 2417 that.events.trigger('checkpoint_failed.Notebook', error);
2411 2418 }
2412 2419 );
2413 2420 };
2414 2421
2415 2422 /**
2416 2423 * Success callback for creating a checkpoint.
2417 2424 *
2418 2425 * @method create_checkpoint_success
2419 2426 * @param {Object} data JSON representation of a checkpoint
2420 2427 */
2421 2428 Notebook.prototype.create_checkpoint_success = function (data) {
2422 2429 this.add_checkpoint(data);
2423 2430 this.events.trigger('checkpoint_created.Notebook', data);
2424 2431 };
2425 2432
2426 2433 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
2427 2434 var that = this;
2428 2435 checkpoint = checkpoint || this.last_checkpoint;
2429 2436 if ( ! checkpoint ) {
2430 2437 console.log("restore dialog, but no checkpoint to restore to!");
2431 2438 return;
2432 2439 }
2433 2440 var body = $('<div/>').append(
2434 2441 $('<p/>').addClass("p-space").text(
2435 2442 "Are you sure you want to revert the notebook to " +
2436 2443 "the latest checkpoint?"
2437 2444 ).append(
2438 2445 $("<strong/>").text(
2439 2446 " This cannot be undone."
2440 2447 )
2441 2448 )
2442 2449 ).append(
2443 2450 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
2444 2451 ).append(
2445 2452 $('<p/>').addClass("p-space").text(
2446 2453 Date(checkpoint.last_modified)
2447 2454 ).css("text-align", "center")
2448 2455 );
2449 2456
2450 2457 dialog.modal({
2451 2458 notebook: this,
2452 2459 keyboard_manager: this.keyboard_manager,
2453 2460 title : "Revert notebook to checkpoint",
2454 2461 body : body,
2455 2462 buttons : {
2456 2463 Revert : {
2457 2464 class : "btn-danger",
2458 2465 click : function () {
2459 2466 that.restore_checkpoint(checkpoint.id);
2460 2467 }
2461 2468 },
2462 2469 Cancel : {}
2463 2470 }
2464 2471 });
2465 2472 };
2466 2473
2467 2474 /**
2468 2475 * Restore the notebook to a checkpoint state.
2469 2476 *
2470 2477 * @method restore_checkpoint
2471 2478 * @param {String} checkpoint ID
2472 2479 */
2473 2480 Notebook.prototype.restore_checkpoint = function (checkpoint) {
2474 2481 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2475 2482 var that = this;
2476 2483 this.contents.restore_checkpoint(this.notebook_path, checkpoint).then(
2477 2484 $.proxy(this.restore_checkpoint_success, this),
2478 2485 function (error) {
2479 2486 that.events.trigger('checkpoint_restore_failed.Notebook', error);
2480 2487 }
2481 2488 );
2482 2489 };
2483 2490
2484 2491 /**
2485 2492 * Success callback for restoring a notebook to a checkpoint.
2486 2493 *
2487 2494 * @method restore_checkpoint_success
2488 2495 */
2489 2496 Notebook.prototype.restore_checkpoint_success = function () {
2490 2497 this.events.trigger('checkpoint_restored.Notebook');
2491 2498 this.load_notebook(this.notebook_path);
2492 2499 };
2493 2500
2494 2501 /**
2495 2502 * Delete a notebook checkpoint.
2496 2503 *
2497 2504 * @method delete_checkpoint
2498 2505 * @param {String} checkpoint ID
2499 2506 */
2500 2507 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2501 2508 this.events.trigger('notebook_restoring.Notebook', checkpoint);
2502 2509 var that = this;
2503 2510 this.contents.delete_checkpoint(this.notebook_path, checkpoint).then(
2504 2511 $.proxy(this.delete_checkpoint_success, this),
2505 2512 function (error) {
2506 2513 that.events.trigger('checkpoint_delete_failed.Notebook', error);
2507 2514 }
2508 2515 );
2509 2516 };
2510 2517
2511 2518 /**
2512 2519 * Success callback for deleting a notebook checkpoint
2513 2520 *
2514 2521 * @method delete_checkpoint_success
2515 2522 */
2516 2523 Notebook.prototype.delete_checkpoint_success = function () {
2517 2524 this.events.trigger('checkpoint_deleted.Notebook');
2518 2525 this.load_notebook(this.notebook_path);
2519 2526 };
2520 2527
2521 2528
2522 2529 // For backwards compatability.
2523 2530 IPython.Notebook = Notebook;
2524 2531
2525 2532 return {'Notebook': Notebook};
2526 2533 });
@@ -1,256 +1,367 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 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/utils",
9 9 "base/js/namespace",
10 ], function (_, Backbone, $, utils, IPython) {
10 "services/kernels/comm"
11 ], function (_, Backbone, $, utils, IPython, comm) {
11 12 "use strict";
12 13 //--------------------------------------------------------------------
13 14 // WidgetManager class
14 15 //--------------------------------------------------------------------
15 16 var WidgetManager = function (comm_manager, notebook) {
16 17 /**
17 18 * Public constructor
18 19 */
19 20 WidgetManager._managers.push(this);
20 21
21 22 // Attach a comm manager to the
22 23 this.keyboard_manager = notebook.keyboard_manager;
23 24 this.notebook = notebook;
24 25 this.comm_manager = comm_manager;
26 this.comm_target_name = 'ipython.widget';
25 27 this._models = {}; /* Dictionary of model ids and model instances */
26 28
27 29 // Register with the comm manager.
28 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
30 this.comm_manager.register_target(this.comm_target_name, $.proxy(this._handle_comm_open, this));
29 31 };
30 32
31 33 //--------------------------------------------------------------------
32 34 // Class level
33 35 //--------------------------------------------------------------------
34 36 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
35 37 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
36 38 WidgetManager._managers = []; /* List of widget managers */
37 39
38 40 WidgetManager.register_widget_model = function (model_name, model_type) {
39 41 // Registers a widget model by name.
40 42 WidgetManager._model_types[model_name] = model_type;
41 43 };
42 44
43 45 WidgetManager.register_widget_view = function (view_name, view_type) {
44 46 // Registers a widget view by name.
45 47 WidgetManager._view_types[view_name] = view_type;
46 48 };
47 49
48 50 //--------------------------------------------------------------------
49 51 // Instance level
50 52 //--------------------------------------------------------------------
51 53 WidgetManager.prototype.display_view = function(msg, model) {
52 54 /**
53 55 * Displays a view for a particular model.
54 56 */
55 57 var that = this;
56 var cell = this.get_msg_cell(msg.parent_header.msg_id);
57 if (cell === null) {
58 return Promise.reject(new Error("Could not determine where the display" +
59 " message was from. Widget will not be displayed"));
60 } else if (cell.widget_subarea) {
61 var dummy = $('<div />');
62 cell.widget_subarea.append(dummy);
63 return this.create_view(model, {cell: cell}).then(
64 function(view) {
58 return new Promise(function(resolve, reject) {
59 var cell = that.get_msg_cell(msg.parent_header.msg_id);
60 if (cell === null) {
61 reject(new Error("Could not determine where the display" +
62 " message was from. Widget will not be displayed"));
63 } else {
64 return that.display_view_in_cell(cell, model);
65 }
66 });
67 };
68
69 WidgetManager.prototype.display_view_in_cell = function(cell, model) {
70 // Displays a view in a cell.
71 return new Promise(function(resolve, reject) {
72 if (cell.display_widget_view) {
73 cell.display_widget_view(that.create_view(model, {cell: cell}))
74 .then(function(view) {
75
65 76 that._handle_display_view(view);
66 dummy.replaceWith(view.$el);
67 77 view.trigger('displayed');
68 return view;
69 }).catch(utils.reject('Could not display view', true));
70 }
78 resolve(view);
79 }, function(error) {
80 reject(new utils.WrappedError('Could not display view', error));
81 });
82 } else {
83 reject(new Error('Cell does not have a `display_widget_view` method.'));
84 }
85 });
71 86 };
72 87
73 88 WidgetManager.prototype._handle_display_view = function (view) {
74 89 /**
75 90 * Have the IPython keyboard manager disable its event
76 91 * handling so the widget can capture keyboard input.
77 92 * Note, this is only done on the outer most widgets.
78 93 */
79 94 if (this.keyboard_manager) {
80 95 this.keyboard_manager.register_events(view.$el);
81 96
82 97 if (view.additional_elements) {
83 98 for (var i = 0; i < view.additional_elements.length; i++) {
84 99 this.keyboard_manager.register_events(view.additional_elements[i]);
85 100 }
86 101 }
87 102 }
88 103 };
89 104
90 105 WidgetManager.prototype.create_view = function(model, options) {
91 106 /**
92 107 * Creates a promise for a view of a given model
93 108 *
94 109 * Make sure the view creation is not out of order with
95 110 * any state updates.
96 111 */
97 112 model.state_change = model.state_change.then(function() {
98 113
99 114 return utils.load_class(model.get('_view_name'), model.get('_view_module'),
100 115 WidgetManager._view_types).then(function(ViewType) {
101 116
102 117 // If a view is passed into the method, use that view's cell as
103 118 // the cell for the view that is created.
104 119 options = options || {};
105 120 if (options.parent !== undefined) {
106 121 options.cell = options.parent.options.cell;
107 122 }
108 123 // Create and render the view...
109 124 var parameters = {model: model, options: options};
110 125 var view = new ViewType(parameters);
111 126 view.listenTo(model, 'destroy', view.remove);
112 127 return Promise.resolve(view.render()).then(function() {return view;});
113 128 }).catch(utils.reject("Couldn't create a view for model id '" + String(model.id) + "'", true));
114 129 });
115 130 model.views[utils.uuid()] = model.state_change;
116 131 return model.state_change;
117 132 };
118 133
119 134 WidgetManager.prototype.get_msg_cell = function (msg_id) {
120 135 var cell = null;
121 136 // First, check to see if the msg was triggered by cell execution.
122 137 if (this.notebook) {
123 138 cell = this.notebook.get_msg_cell(msg_id);
124 139 }
125 140 if (cell !== null) {
126 141 return cell;
127 142 }
128 143 // Second, check to see if a get_cell callback was defined
129 144 // for the message. get_cell callbacks are registered for
130 145 // widget messages, so this block is actually checking to see if the
131 146 // message was triggered by a widget.
132 147 var kernel = this.comm_manager.kernel;
133 148 if (kernel) {
134 149 var callbacks = kernel.get_callbacks_for_msg(msg_id);
135 150 if (callbacks && callbacks.iopub &&
136 151 callbacks.iopub.get_cell !== undefined) {
137 152 return callbacks.iopub.get_cell();
138 153 }
139 154 }
140 155
141 156 // Not triggered by a cell or widget (no get_cell callback
142 157 // exists).
143 158 return null;
144 159 };
145 160
146 161 WidgetManager.prototype.callbacks = function (view) {
147 162 /**
148 163 * callback handlers specific a view
149 164 */
150 165 var callbacks = {};
151 166 if (view && view.options.cell) {
152 167
153 168 // Try to get output handlers
154 169 var cell = view.options.cell;
155 170 var handle_output = null;
156 171 var handle_clear_output = null;
157 172 if (cell.output_area) {
158 173 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
159 174 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
160 175 }
161 176
162 177 // Create callback dictionary using what is known
163 178 var that = this;
164 179 callbacks = {
165 180 iopub : {
166 181 output : handle_output,
167 182 clear_output : handle_clear_output,
168 183
169 184 // Special function only registered by widget messages.
170 185 // Allows us to get the cell for a message so we know
171 186 // where to add widgets if the code requires it.
172 187 get_cell : function () {
173 188 return cell;
174 189 },
175 190 },
176 191 };
177 192 }
178 193 return callbacks;
179 194 };
180 195
181 196 WidgetManager.prototype.get_model = function (model_id) {
182 197 /**
183 198 * Get a promise for a model by model id.
184 199 */
185 200 return this._models[model_id];
186 201 };
187 202
188 203 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
189 204 /**
190 205 * Handle when a comm is opened.
191 206 */
192 207 return this.create_model({
193 208 model_name: msg.content.data.model_name,
194 209 model_module: msg.content.data.model_module,
195 210 comm: comm}).catch(utils.reject("Couldn't create a model.", true));
196 211 };
197 212
198 213 WidgetManager.prototype.create_model = function (options) {
199 214 /**
200 215 * Create and return a promise for a new widget model
201 216 *
202 217 * Minimally, one must provide the model_name and widget_class
203 218 * parameters to create a model from Javascript.
204 219 *
205 220 * Example
206 221 * --------
207 222 * JS:
208 223 * IPython.notebook.kernel.widget_manager.create_model({
209 224 * model_name: 'WidgetModel',
210 225 * widget_class: 'IPython.html.widgets.widget_int.IntSlider'})
211 226 * .then(function(model) { console.log('Create success!', model); },
212 227 * $.proxy(console.error, console));
213 228 *
214 229 * Parameters
215 230 * ----------
216 231 * options: dictionary
217 232 * Dictionary of options with the following contents:
218 233 * model_name: string
219 234 * Target name of the widget model to create.
220 235 * model_module: (optional) string
221 236 * Module name of the widget model to create.
222 237 * widget_class: (optional) string
223 238 * Target name of the widget in the back-end.
224 239 * comm: (optional) Comm
225 240 *
226 241 * Create a comm if it wasn't provided.
227 242 */
228 243 var comm = options.comm;
229 244 if (!comm) {
230 245 comm = this.comm_manager.new_comm('ipython.widget', {'widget_class': options.widget_class});
231 246 }
232 247
233 248 var that = this;
234 249 var model_id = comm.comm_id;
235 250 var model_promise = utils.load_class(options.model_name, options.model_module, WidgetManager._model_types)
236 251 .then(function(ModelType) {
237 252 var widget_model = new ModelType(that, model_id, comm);
238 253 widget_model.once('comm:close', function () {
239 254 delete that._models[model_id];
240 255 });
256 widget_model.name = options.model_name;
257 widget_model.module = options.model_module;
241 258 return widget_model;
242 259
243 260 }, function(error) {
244 261 delete that._models[model_id];
245 262 var wrapped_error = new utils.WrappedError("Couldn't create model", error);
246 263 return Promise.reject(wrapped_error);
247 264 });
248 265 this._models[model_id] = model_promise;
249 266 return model_promise;
250 267 };
251 268
269 WidgetManager.prototype.get_state = function(options) {
270 // Get the state of the widget manager.
271 //
272 // This includes all of the widget models and the cells that they are
273 // displayed in.
274 //
275 // Parameters
276 // ----------
277 // options: dictionary
278 // Dictionary of options with the following contents:
279 // only_displayed: (optional) boolean=false
280 // Only return models with one or more displayed views.
281 // not_alive: (optional) boolean=false
282 // Include models that have comms with severed connections.
283 return utils.resolve_promise_dict(function(models) {
284 var state = {};
285 for (var model_id in models) {
286 if (models.hasOwnProperty(model_id)) {
287 var model = models[model_id];
288
289 // If the model has one or more views defined for it,
290 // consider it displayed.
291 var displayed_flag = !(options && options.only_displayed) || Object.keys(model.views).length > 0;
292 var alive_flag = (options && options.not_alive) || model.comm_alive;
293 if (displayed_flag && alive_flag) {
294 state[model.model_id] = {
295 model_name: model.name,
296 model_module: model.module,
297 views: [],
298 };
299
300 // Get the views that are displayed *now*.
301 for (var id in model.views) {
302 if (model.views.hasOwnProperty(id)) {
303 var view = model.views[id];
304 var cell_index = this.notebook.find_cell_index(view.options.cell);
305 state[model.model_id].views.push(cell_index);
306 }
307 }
308 }
309 }
310 }
311 return state;
312 });
313 };
314
315 WidgetManager.prototype.set_state = function(state) {
316 // Set the notebook's state.
317 //
318 // Reconstructs all of the widget models and attempts to redisplay the
319 // widgets in the appropriate cells by cell index.
320
321 // Get the kernel when it's available.
322 var that = this;
323 return (new Promise(function(resolve, reject) {
324 if (that.kernel) {
325 resolve(that.kernel);
326 } else {
327 that.events.on('kernel_created.Session', function(event, data) {
328 resolve(data.kernel);
329 });
330 }
331 })).then(function(kernel) {
332
333 // Recreate all the widget models for the given state.
334 that.widget_models = [];
335 for (var i = 0; i < state.length; i++) {
336 // Recreate a comm using the widget's model id (model_id == comm_id).
337 var new_comm = new comm.Comm(kernel.widget_manager.comm_target_name, state[i].model_id);
338 kernel.comm_manager.register_comm(new_comm);
339
340 // Create the model using the recreated comm. When the model is
341 // created we don't know yet if the comm is valid so set_comm_alive
342 // false. Once we receive the first state push from the back-end
343 // we know the comm is alive.
344 var model = kernel.widget_manager.create_model({
345 comm: new_comm,
346 model_name: state[i].model_name,
347 model_module: state[i].model_module}).then(function(model) {
348 model.set_comm_alive(false);
349 model.request_state();
350 model.received_state.then(function() {
351 model.set_comm_alive(true);
352 });
353 return model;
354 });
355 that.widget_models.push(model);
356 }
357 return Promise.all(that.widget_models);
358
359 });
360
361 };
362
252 363 // Backwards compatibility.
253 364 IPython.WidgetManager = WidgetManager;
254 365
255 366 return {'WidgetManager': WidgetManager};
256 367 });
@@ -1,682 +1,747 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/utils",
9 9 "base/js/namespace",
10 10 ], function(widgetmanager, _, Backbone, $, utils, IPython){
11 11
12 12 var WidgetModel = Backbone.Model.extend({
13 13 constructor: function (widget_manager, model_id, comm) {
14 14 /**
15 15 * Constructor
16 16 *
17 17 * Creates a WidgetModel instance.
18 18 *
19 19 * Parameters
20 20 * ----------
21 21 * widget_manager : WidgetManager instance
22 22 * model_id : string
23 23 * An ID unique to this model.
24 24 * comm : Comm instance (optional)
25 25 */
26 26 this.widget_manager = widget_manager;
27 27 this.state_change = Promise.resolve();
28 28 this._buffered_state_diff = {};
29 29 this.pending_msgs = 0;
30 30 this.msg_buffer = null;
31 31 this.state_lock = null;
32 32 this.id = model_id;
33 33 this.views = {};
34 34
35 // Promise that is resolved when a state is received
36 // from the back-end.
37 var that = this;
38 this.received_state = new Promise(function(resolve) {
39 that._resolve_received_state = resolve;
40 });
41
35 42 if (comm !== undefined) {
36 43 // Remember comm associated with the model.
37 44 this.comm = comm;
38 45 comm.model = this;
39 46
40 47 // Hook comm messages up to model.
41 48 comm.on_close($.proxy(this._handle_comm_closed, this));
42 49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50
51 // Assume the comm is alive.
52 this.set_comm_alive(true);
53 } else {
54 this.set_comm_alive(false);
43 55 }
44 56 return Backbone.Model.apply(this);
45 57 },
46 58
47 59 send: function (content, callbacks) {
48 60 /**
49 61 * Send a custom msg over the comm.
50 62 */
51 63 if (this.comm !== undefined) {
52 64 var data = {method: 'custom', content: content};
53 65 this.comm.send(data, callbacks);
54 66 this.pending_msgs++;
55 67 }
56 68 },
57 69
58 _handle_comm_closed: function (msg) {
70 request_state: function(callbacks) {
71 /**
72 * Request a state push from the back-end.
73 */
74 if (!this.comm) {
75 console.error("Could not request_state because comm doesn't exist!");
76 return;
77 }
78 this.comm.send({method: 'request_state'}, callbacks || this.widget_manager.callbacks());
79 },
80
81 set_comm_alive: function(alive) {
82 /**
83 * Change the comm_alive state of the model.
84 */
85 if (this.comm_alive === undefined || this.comm_alive != alive) {
86 this.comm_alive = alive;
87 this.trigger(alive ? 'comm_is_live' : 'comm_is_dead', {model: this});
88 }
89 },
90
91 close: function(comm_closed) {
59 92 /**
60 * Handle when a widget is closed.
93 * Close model
61 94 */
62 this.trigger('comm:close');
95 if (this.comm && !comm_closed) {
96 this.comm.close();
97 }
63 98 this.stopListening();
64 99 this.trigger('destroy', this);
65 100 delete this.comm.model; // Delete ref so GC will collect widget model.
66 101 delete this.comm;
67 102 delete this.model_id; // Delete id from model so widget manager cleans up.
68 103 _.each(this.views, function(v, id, views) {
69 104 v.then(function(view) {
70 105 view.remove();
71 106 delete views[id];
72 107 });
73 108 });
74 109 },
75 110
111 _handle_comm_closed: function (msg) {
112 /**
113 * Handle when a widget is closed.
114 */
115 this.trigger('comm:close');
116 this.close(true);
117 },
118
76 119 _handle_comm_msg: function (msg) {
77 120 /**
78 121 * Handle incoming comm msg.
79 122 */
80 123 var method = msg.content.data.method;
81 124 var that = this;
82 125 switch (method) {
83 126 case 'update':
84 127 this.state_change = this.state_change.then(function() {
85 128 return that.set_state(msg.content.data.state);
86 129 }).catch(utils.reject("Couldn't process update msg for model id '" + String(that.id) + "'", true));
87 130 break;
88 131 case 'custom':
89 132 this.trigger('msg:custom', msg.content.data.content);
90 133 break;
91 134 case 'display':
92 135 this.widget_manager.display_view(msg, this);
93 136 break;
94 137 }
95 138 },
96 139
97 140 set_state: function (state) {
98 141 var that = this;
99 142 // Handle when a widget is updated via the python side.
100 143 return this._unpack_models(state).then(function(state) {
101 144 that.state_lock = state;
102 145 try {
103 146 WidgetModel.__super__.set.call(that, state);
104 147 } finally {
105 148 that.state_lock = null;
106 149 }
107 }).catch(utils.reject("Couldn't set model state", true));
150 that._resolve_received_state();
151 return Promise.resolve();
152 }, utils.reject("Couldn't set model state", true));
153 },
154
155 get_state: function() {
156 // Get the serializable state of the model.
157 state = this.toJSON();
158 for (var key in state) {
159 if (state.hasOwnProperty(key)) {
160 state[key] = this._pack_models(state[key]);
161 }
162 }
163 return state;
108 164 },
109 165
110 166 _handle_status: function (msg, callbacks) {
111 167 /**
112 168 * Handle status msgs.
113 169 *
114 170 * execution_state : ('busy', 'idle', 'starting')
115 171 */
116 172 if (this.comm !== undefined) {
117 173 if (msg.content.execution_state ==='idle') {
118 174 // Send buffer if this message caused another message to be
119 175 // throttled.
120 176 if (this.msg_buffer !== null &&
121 177 (this.get('msg_throttle') || 3) === this.pending_msgs) {
122 178 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
123 179 this.comm.send(data, callbacks);
124 180 this.msg_buffer = null;
125 181 } else {
126 182 --this.pending_msgs;
127 183 }
128 184 }
129 185 }
130 186 },
131 187
132 188 callbacks: function(view) {
133 189 /**
134 190 * Create msg callbacks for a comm msg.
135 191 */
136 192 var callbacks = this.widget_manager.callbacks(view);
137 193
138 194 if (callbacks.iopub === undefined) {
139 195 callbacks.iopub = {};
140 196 }
141 197
142 198 var that = this;
143 199 callbacks.iopub.status = function (msg) {
144 200 that._handle_status(msg, callbacks);
145 201 };
146 202 return callbacks;
147 203 },
148 204
149 205 set: function(key, val, options) {
150 206 /**
151 207 * Set a value.
152 208 */
153 209 var return_value = WidgetModel.__super__.set.apply(this, arguments);
154 210
155 211 // Backbone only remembers the diff of the most recent set()
156 212 // operation. Calling set multiple times in a row results in a
157 213 // loss of diff information. Here we keep our own running diff.
158 214 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
159 215 return return_value;
160 216 },
161 217
162 218 sync: function (method, model, options) {
163 219 /**
164 220 * Handle sync to the back-end. Called when a model.save() is called.
165 221 *
166 222 * Make sure a comm exists.
167 223 */
168 224 var error = options.error || function() {
169 225 console.error('Backbone sync error:', arguments);
170 226 };
171 227 if (this.comm === undefined) {
172 228 error();
173 229 return false;
174 230 }
175 231
176 232 // Delete any key value pairs that the back-end already knows about.
177 233 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
178 234 if (this.state_lock !== null) {
179 235 var keys = Object.keys(this.state_lock);
180 236 for (var i=0; i<keys.length; i++) {
181 237 var key = keys[i];
182 238 if (attrs[key] === this.state_lock[key]) {
183 239 delete attrs[key];
184 240 }
185 241 }
186 242 }
187 243
188 244 // Only sync if there are attributes to send to the back-end.
189 245 attrs = this._pack_models(attrs);
190 246 if (_.size(attrs) > 0) {
191 247
192 248 // If this message was sent via backbone itself, it will not
193 249 // have any callbacks. It's important that we create callbacks
194 250 // so we can listen for status messages, etc...
195 251 var callbacks = options.callbacks || this.callbacks();
196 252
197 253 // Check throttle.
198 254 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
199 255 // The throttle has been exceeded, buffer the current msg so
200 256 // it can be sent once the kernel has finished processing
201 257 // some of the existing messages.
202 258
203 259 // Combine updates if it is a 'patch' sync, otherwise replace updates
204 260 switch (method) {
205 261 case 'patch':
206 262 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
207 263 break;
208 264 case 'update':
209 265 case 'create':
210 266 this.msg_buffer = attrs;
211 267 break;
212 268 default:
213 269 error();
214 270 return false;
215 271 }
216 272 this.msg_buffer_callbacks = callbacks;
217 273
218 274 } else {
219 275 // We haven't exceeded the throttle, send the message like
220 276 // normal.
221 277 var data = {method: 'backbone', sync_data: attrs};
222 278 this.comm.send(data, callbacks);
223 279 this.pending_msgs++;
224 280 }
225 281 }
226 282 // Since the comm is a one-way communication, assume the message
227 283 // arrived. Don't call success since we don't have a model back from the server
228 284 // this means we miss out on the 'sync' event.
229 285 this._buffered_state_diff = {};
230 286 },
231 287
232 288 save_changes: function(callbacks) {
233 289 /**
234 290 * Push this model's state to the back-end
235 291 *
236 292 * This invokes a Backbone.Sync.
237 293 */
238 294 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
239 295 },
240 296
241 297 _pack_models: function(value) {
242 298 /**
243 299 * Replace models with model ids recursively.
244 300 */
245 301 var that = this;
246 302 var packed;
247 303 if (value instanceof Backbone.Model) {
248 304 return "IPY_MODEL_" + value.id;
249 305
250 306 } else if ($.isArray(value)) {
251 307 packed = [];
252 308 _.each(value, function(sub_value, key) {
253 309 packed.push(that._pack_models(sub_value));
254 310 });
255 311 return packed;
256 312 } else if (value instanceof Date || value instanceof String) {
257 313 return value;
258 314 } else if (value instanceof Object) {
259 315 packed = {};
260 316 _.each(value, function(sub_value, key) {
261 317 packed[key] = that._pack_models(sub_value);
262 318 });
263 319 return packed;
264 320
265 321 } else {
266 322 return value;
267 323 }
268 324 },
269 325
270 326 _unpack_models: function(value) {
271 327 /**
272 328 * Replace model ids with models recursively.
273 329 */
274 330 var that = this;
275 331 var unpacked;
276 332 if ($.isArray(value)) {
277 333 unpacked = [];
278 334 _.each(value, function(sub_value, key) {
279 335 unpacked.push(that._unpack_models(sub_value));
280 336 });
281 337 return Promise.all(unpacked);
282 338 } else if (value instanceof Object) {
283 339 unpacked = {};
284 340 _.each(value, function(sub_value, key) {
285 341 unpacked[key] = that._unpack_models(sub_value);
286 342 });
287 343 return utils.resolve_promises_dict(unpacked);
288 344 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
289 345 // get_model returns a promise already
290 346 return this.widget_manager.get_model(value.slice(10, value.length));
291 347 } else {
292 348 return Promise.resolve(value);
293 349 }
294 350 },
295 351
296 352 on_some_change: function(keys, callback, context) {
297 353 /**
298 354 * on_some_change(["key1", "key2"], foo, context) differs from
299 355 * on("change:key1 change:key2", foo, context).
300 356 * If the widget attributes key1 and key2 are both modified,
301 357 * the second form will result in foo being called twice
302 358 * while the first will call foo only once.
303 359 */
304 360 this.on('change', function() {
305 361 if (keys.some(this.hasChanged, this)) {
306 362 callback.apply(context);
307 363 }
308 364 }, this);
309 365
310 366 },
311 367 });
312 368 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
313 369
314 370
315 371 var WidgetView = Backbone.View.extend({
316 372 initialize: function(parameters) {
317 373 /**
318 374 * Public constructor.
319 375 */
320 376 this.model.on('change',this.update,this);
321 377 this.options = parameters.options;
322 378 this.on('displayed', function() {
323 379 this.is_displayed = true;
324 380 }, this);
381 this.on('remove', function() {
382 delete this.model.views[this.id];
383 }, this);
325 384 },
326 385
327 386 update: function(){
328 387 /**
329 388 * Triggered on model change.
330 389 *
331 390 * Update view to be consistent with this.model
332 391 */
333 392 },
334 393
335 394 create_child_view: function(child_model, options) {
336 395 /**
337 396 * Create and promise that resolves to a child view of a given model
338 397 */
339 398 var that = this;
340 399 options = $.extend({ parent: this }, options || {});
341 400 return this.model.widget_manager.create_view(child_model, options).catch(utils.reject("Couldn't create child view"), true);
342 401 },
343 402
344 403 callbacks: function(){
345 404 /**
346 405 * Create msg callbacks for a comm msg.
347 406 */
348 407 return this.model.callbacks(this);
349 408 },
350 409
351 410 render: function(){
352 411 /**
353 412 * Render the view.
354 413 *
355 414 * By default, this is only called the first time the view is created
356 415 */
357 416 },
358 417
359 418 show: function(){
360 419 /**
361 420 * Show the widget-area
362 421 */
363 422 if (this.options && this.options.cell &&
364 423 this.options.cell.widget_area !== undefined) {
365 424 this.options.cell.widget_area.show();
366 425 }
367 426 },
368 427
369 428 send: function (content) {
370 429 /**
371 430 * Send a custom msg associated with this view.
372 431 */
373 432 this.model.send(content, this.callbacks());
374 433 },
375 434
376 435 touch: function () {
377 436 this.model.save_changes(this.callbacks());
378 437 },
379 438
380 439 after_displayed: function (callback, context) {
381 440 /**
382 441 * Calls the callback right away is the view is already displayed
383 442 * otherwise, register the callback to the 'displayed' event.
384 443 */
385 444 if (this.is_displayed) {
386 445 callback.apply(context);
387 446 } else {
388 447 this.on('displayed', callback, context);
389 448 }
449 },
450
451 remove: function () {
452 // Raise a remove event when the view is removed.
453 WidgetView.__super__.remove.apply(this, arguments);
454 this.trigger('remove');
390 455 }
391 456 });
392 457
393 458
394 459 var DOMWidgetView = WidgetView.extend({
395 460 initialize: function (parameters) {
396 461 /**
397 462 * Public constructor
398 463 */
399 464 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
400 465 this.on('displayed', this.show, this);
401 466 this.model.on('change:visible', this.update_visible, this);
402 467 this.model.on('change:_css', this.update_css, this);
403 468
404 469 this.model.on('change:_dom_classes', function(model, new_classes) {
405 470 var old_classes = model.previous('_dom_classes');
406 471 this.update_classes(old_classes, new_classes);
407 472 }, this);
408 473
409 474 this.model.on('change:color', function (model, value) {
410 475 this.update_attr('color', value); }, this);
411 476
412 477 this.model.on('change:background_color', function (model, value) {
413 478 this.update_attr('background', value); }, this);
414 479
415 480 this.model.on('change:width', function (model, value) {
416 481 this.update_attr('width', value); }, this);
417 482
418 483 this.model.on('change:height', function (model, value) {
419 484 this.update_attr('height', value); }, this);
420 485
421 486 this.model.on('change:border_color', function (model, value) {
422 487 this.update_attr('border-color', value); }, this);
423 488
424 489 this.model.on('change:border_width', function (model, value) {
425 490 this.update_attr('border-width', value); }, this);
426 491
427 492 this.model.on('change:border_style', function (model, value) {
428 493 this.update_attr('border-style', value); }, this);
429 494
430 495 this.model.on('change:font_style', function (model, value) {
431 496 this.update_attr('font-style', value); }, this);
432 497
433 498 this.model.on('change:font_weight', function (model, value) {
434 499 this.update_attr('font-weight', value); }, this);
435 500
436 501 this.model.on('change:font_size', function (model, value) {
437 502 this.update_attr('font-size', this._default_px(value)); }, this);
438 503
439 504 this.model.on('change:font_family', function (model, value) {
440 505 this.update_attr('font-family', value); }, this);
441 506
442 507 this.model.on('change:padding', function (model, value) {
443 508 this.update_attr('padding', value); }, this);
444 509
445 510 this.model.on('change:margin', function (model, value) {
446 511 this.update_attr('margin', this._default_px(value)); }, this);
447 512
448 513 this.model.on('change:border_radius', function (model, value) {
449 514 this.update_attr('border-radius', this._default_px(value)); }, this);
450 515
451 516 this.after_displayed(function() {
452 517 this.update_visible(this.model, this.model.get("visible"));
453 518 this.update_classes([], this.model.get('_dom_classes'));
454 519
455 520 this.update_attr('color', this.model.get('color'));
456 521 this.update_attr('background', this.model.get('background_color'));
457 522 this.update_attr('width', this.model.get('width'));
458 523 this.update_attr('height', this.model.get('height'));
459 524 this.update_attr('border-color', this.model.get('border_color'));
460 525 this.update_attr('border-width', this.model.get('border_width'));
461 526 this.update_attr('border-style', this.model.get('border_style'));
462 527 this.update_attr('font-style', this.model.get('font_style'));
463 528 this.update_attr('font-weight', this.model.get('font_weight'));
464 529 this.update_attr('font-size', this.model.get('font_size'));
465 530 this.update_attr('font-family', this.model.get('font_family'));
466 531 this.update_attr('padding', this.model.get('padding'));
467 532 this.update_attr('margin', this.model.get('margin'));
468 533 this.update_attr('border-radius', this.model.get('border_radius'));
469 534
470 535 this.update_css(this.model, this.model.get("_css"));
471 536 }, this);
472 537 },
473 538
474 539 _default_px: function(value) {
475 540 /**
476 541 * Makes browser interpret a numerical string as a pixel value.
477 542 */
478 543 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
479 544 return value.trim() + 'px';
480 545 }
481 546 return value;
482 547 },
483 548
484 549 update_attr: function(name, value) {
485 550 /**
486 551 * Set a css attr of the widget view.
487 552 */
488 553 this.$el.css(name, value);
489 554 },
490 555
491 556 update_visible: function(model, value) {
492 557 /**
493 558 * Update visibility
494 559 */
495 560 this.$el.toggle(value);
496 561 },
497 562
498 563 update_css: function (model, css) {
499 564 /**
500 565 * Update the css styling of this view.
501 566 */
502 567 var e = this.$el;
503 568 if (css === undefined) {return;}
504 569 for (var i = 0; i < css.length; i++) {
505 570 // Apply the css traits to all elements that match the selector.
506 571 var selector = css[i][0];
507 572 var elements = this._get_selector_element(selector);
508 573 if (elements.length > 0) {
509 574 var trait_key = css[i][1];
510 575 var trait_value = css[i][2];
511 576 elements.css(trait_key ,trait_value);
512 577 }
513 578 }
514 579 },
515 580
516 581 update_classes: function (old_classes, new_classes, $el) {
517 582 /**
518 583 * Update the DOM classes applied to an element, default to this.$el.
519 584 */
520 585 if ($el===undefined) {
521 586 $el = this.$el;
522 587 }
523 588 _.difference(old_classes, new_classes).map(function(c) {$el.removeClass(c);})
524 589 _.difference(new_classes, old_classes).map(function(c) {$el.addClass(c);})
525 590 },
526 591
527 592 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
528 593 /**
529 594 * Update the DOM classes applied to the widget based on a single
530 595 * trait's value.
531 596 *
532 597 * Given a trait value classes map, this function automatically
533 598 * handles applying the appropriate classes to the widget element
534 599 * and removing classes that are no longer valid.
535 600 *
536 601 * Parameters
537 602 * ----------
538 603 * class_map: dictionary
539 604 * Dictionary of trait values to class lists.
540 605 * Example:
541 606 * {
542 607 * success: ['alert', 'alert-success'],
543 608 * info: ['alert', 'alert-info'],
544 609 * warning: ['alert', 'alert-warning'],
545 610 * danger: ['alert', 'alert-danger']
546 611 * };
547 612 * trait_name: string
548 613 * Name of the trait to check the value of.
549 614 * previous_trait_value: optional string, default ''
550 615 * Last trait value
551 616 * $el: optional jQuery element handle, defaults to this.$el
552 617 * Element that the classes are applied to.
553 618 */
554 619 var key = previous_trait_value;
555 620 if (key === undefined) {
556 621 key = this.model.previous(trait_name);
557 622 }
558 623 var old_classes = class_map[key] ? class_map[key] : [];
559 624 key = this.model.get(trait_name);
560 625 var new_classes = class_map[key] ? class_map[key] : [];
561 626
562 627 this.update_classes(old_classes, new_classes, $el || this.$el);
563 628 },
564 629
565 630 _get_selector_element: function (selector) {
566 631 /**
567 632 * Get the elements via the css selector.
568 633 */
569 634 var elements;
570 635 if (!selector) {
571 636 elements = this.$el;
572 637 } else {
573 638 elements = this.$el.find(selector).addBack(selector);
574 639 }
575 640 return elements;
576 641 },
577 642
578 643 typeset: function(element, text){
579 644 utils.typeset.apply(null, arguments);
580 645 },
581 646 });
582 647
583 648
584 649 var ViewList = function(create_view, remove_view, context) {
585 650 /**
586 651 * - create_view and remove_view are default functions called when adding or removing views
587 652 * - create_view takes a model and returns a view or a promise for a view for that model
588 653 * - remove_view takes a view and destroys it (including calling `view.remove()`)
589 654 * - each time the update() function is called with a new list, the create and remove
590 655 * callbacks will be called in an order so that if you append the views created in the
591 656 * create callback and remove the views in the remove callback, you will duplicate
592 657 * the order of the list.
593 658 * - the remove callback defaults to just removing the view (e.g., pass in null for the second parameter)
594 659 * - the context defaults to the created ViewList. If you pass another context, the create and remove
595 660 * will be called in that context.
596 661 */
597 662
598 663 this.initialize.apply(this, arguments);
599 664 };
600 665
601 666 _.extend(ViewList.prototype, {
602 667 initialize: function(create_view, remove_view, context) {
603 668 this.state_change = Promise.resolve();
604 669 this._handler_context = context || this;
605 670 this._models = [];
606 671 this.views = [];
607 672 this._create_view = create_view;
608 673 this._remove_view = remove_view || function(view) {view.remove();};
609 674 },
610 675
611 676 update: function(new_models, create_view, remove_view, context) {
612 677 /**
613 678 * the create_view, remove_view, and context arguments override the defaults
614 679 * specified when the list is created.
615 680 * returns a promise that resolves after this update is done
616 681 */
617 682 var remove = remove_view || this._remove_view;
618 683 var create = create_view || this._create_view;
619 684 if (create === undefined || remove === undefined){
620 685 console.error("Must define a create a remove function");
621 686 }
622 687 var context = context || this._handler_context;
623 688 var added_views = [];
624 689 var that = this;
625 690 this.state_change = this.state_change.then(function() {
626 691 var i;
627 692 // first, skip past the beginning of the lists if they are identical
628 693 for (i = 0; i < new_models.length; i++) {
629 694 if (i >= that._models.length || new_models[i] !== that._models[i]) {
630 695 break;
631 696 }
632 697 }
633 698 var first_removed = i;
634 699 // Remove the non-matching items from the old list.
635 700 for (var j = first_removed; j < that._models.length; j++) {
636 701 remove.call(context, that.views[j]);
637 702 }
638 703
639 704 // Add the rest of the new list items.
640 705 for (; i < new_models.length; i++) {
641 706 added_views.push(create.call(context, new_models[i]));
642 707 }
643 708 // make a copy of the input array
644 709 that._models = new_models.slice();
645 710 return Promise.all(added_views).then(function(added) {
646 711 Array.prototype.splice.apply(that.views, [first_removed, that.views.length].concat(added));
647 712 return that.views;
648 713 });
649 714 });
650 715 return this.state_change;
651 716 },
652 717
653 718 remove: function() {
654 719 /**
655 720 * removes every view in the list; convenience function for `.update([])`
656 721 * that should be faster
657 722 * returns a promise that resolves after this removal is done
658 723 */
659 724 var that = this;
660 725 this.state_change = this.state_change.then(function() {
661 726 for (var i = 0; i < that.views.length; i++) {
662 727 that._remove_view.call(that._handler_context, that.views[i]);
663 728 }
664 729 that._models = [];
665 730 that.views = [];
666 731 });
667 732 return this.state_change;
668 733 },
669 734 });
670 735
671 736 var widget = {
672 737 'WidgetModel': WidgetModel,
673 738 'WidgetView': WidgetView,
674 739 'DOMWidgetView': DOMWidgetView,
675 740 'ViewList': ViewList,
676 741 };
677 742
678 743 // For backwards compatability.
679 744 $.extend(IPython, widget);
680 745
681 746 return widget;
682 747 });
@@ -1,481 +1,488 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.importstring import import_item
22 22 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
23 23 CaselessStrEnum, Tuple, CUnicode, Int, Set
24 24 from IPython.utils.py3compat import string_types
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Classes
28 28 #-----------------------------------------------------------------------------
29 29 class CallbackDispatcher(LoggingConfigurable):
30 30 """A structure for registering and running callbacks"""
31 31 callbacks = List()
32 32
33 33 def __call__(self, *args, **kwargs):
34 34 """Call all of the registered callbacks."""
35 35 value = None
36 36 for callback in self.callbacks:
37 37 try:
38 38 local_value = callback(*args, **kwargs)
39 39 except Exception as e:
40 40 ip = get_ipython()
41 41 if ip is None:
42 42 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
43 43 else:
44 44 ip.showtraceback()
45 45 else:
46 46 value = local_value if local_value is not None else value
47 47 return value
48 48
49 49 def register_callback(self, callback, remove=False):
50 50 """(Un)Register a callback
51 51
52 52 Parameters
53 53 ----------
54 54 callback: method handle
55 55 Method to be registered or unregistered.
56 56 remove=False: bool
57 57 Whether to unregister the callback."""
58 58
59 59 # (Un)Register the callback.
60 60 if remove and callback in self.callbacks:
61 61 self.callbacks.remove(callback)
62 62 elif not remove and callback not in self.callbacks:
63 63 self.callbacks.append(callback)
64 64
65 65 def _show_traceback(method):
66 66 """decorator for showing tracebacks in IPython"""
67 67 def m(self, *args, **kwargs):
68 68 try:
69 69 return(method(self, *args, **kwargs))
70 70 except Exception as e:
71 71 ip = get_ipython()
72 72 if ip is None:
73 73 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
74 74 else:
75 75 ip.showtraceback()
76 76 return m
77 77
78 78
79 79 def register(key=None):
80 80 """Returns a decorator registering a widget class in the widget registry.
81 81 If no key is provided, the class name is used as a key. A key is
82 82 provided for each core IPython widget so that the frontend can use
83 83 this key regardless of the language of the kernel"""
84 84 def wrap(widget):
85 85 l = key if key is not None else widget.__module__ + widget.__name__
86 86 Widget.widget_types[l] = widget
87 87 return widget
88 88 return wrap
89 89
90 90
91 91 class Widget(LoggingConfigurable):
92 92 #-------------------------------------------------------------------------
93 93 # Class attributes
94 94 #-------------------------------------------------------------------------
95 95 _widget_construction_callback = None
96 96 widgets = {}
97 97 widget_types = {}
98 98
99 99 @staticmethod
100 100 def on_widget_constructed(callback):
101 101 """Registers a callback to be called when a widget is constructed.
102 102
103 103 The callback must have the following signature:
104 104 callback(widget)"""
105 105 Widget._widget_construction_callback = callback
106 106
107 107 @staticmethod
108 108 def _call_widget_constructed(widget):
109 109 """Static method, called when a widget is constructed."""
110 110 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
111 111 Widget._widget_construction_callback(widget)
112 112
113 113 @staticmethod
114 114 def handle_comm_opened(comm, msg):
115 115 """Static method, called when a widget is constructed."""
116 116 widget_class = import_item(msg['content']['data']['widget_class'])
117 117 widget = widget_class(comm=comm)
118 118
119 119
120 120 #-------------------------------------------------------------------------
121 121 # Traits
122 122 #-------------------------------------------------------------------------
123 123 _model_module = Unicode(None, allow_none=True, help="""A requirejs module name
124 124 in which to find _model_name. If empty, look in the global registry.""")
125 125 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
126 126 registered in the front-end to create and sync this widget with.""")
127 127 _view_module = Unicode(help="""A requirejs module in which to find _view_name.
128 128 If empty, look in the global registry.""", sync=True)
129 129 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
130 130 to use to represent the widget.""", sync=True)
131 131 comm = Instance('IPython.kernel.comm.Comm')
132 132
133 133 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
134 134 front-end can send before receiving an idle msg from the back-end.""")
135 135
136 136 version = Int(0, sync=True, help="""Widget's version""")
137 137 keys = List()
138 138 def _keys_default(self):
139 139 return [name for name in self.traits(sync=True)]
140 140
141 141 _property_lock = Tuple((None, None))
142 142 _send_state_lock = Int(0)
143 143 _states_to_send = Set(allow_none=False)
144 144 _display_callbacks = Instance(CallbackDispatcher, ())
145 145 _msg_callbacks = Instance(CallbackDispatcher, ())
146 146
147 147 #-------------------------------------------------------------------------
148 148 # (Con/de)structor
149 149 #-------------------------------------------------------------------------
150 150 def __init__(self, **kwargs):
151 151 """Public constructor"""
152 152 self._model_id = kwargs.pop('model_id', None)
153 153 super(Widget, self).__init__(**kwargs)
154 154
155 155 Widget._call_widget_constructed(self)
156 156 self.open()
157 157
158 158 def __del__(self):
159 159 """Object disposal"""
160 160 self.close()
161 161
162 162 #-------------------------------------------------------------------------
163 163 # Properties
164 164 #-------------------------------------------------------------------------
165 165
166 166 def open(self):
167 167 """Open a comm to the frontend if one isn't already open."""
168 168 if self.comm is None:
169 169 args = dict(target_name='ipython.widget',
170 170 data={'model_name': self._model_name,
171 171 'model_module': self._model_module})
172 172 if self._model_id is not None:
173 173 args['comm_id'] = self._model_id
174 174 self.comm = Comm(**args)
175 175
176 176 def _comm_changed(self, name, new):
177 177 """Called when the comm is changed."""
178 178 if new is None:
179 179 return
180 180 self._model_id = self.model_id
181 181
182 182 self.comm.on_msg(self._handle_msg)
183 183 Widget.widgets[self.model_id] = self
184 184
185 185 # first update
186 186 self.send_state()
187 187
188 188 @property
189 189 def model_id(self):
190 190 """Gets the model id of this widget.
191 191
192 192 If a Comm doesn't exist yet, a Comm will be created automagically."""
193 193 return self.comm.comm_id
194 194
195 195 #-------------------------------------------------------------------------
196 196 # Methods
197 197 #-------------------------------------------------------------------------
198 198
199 199 def close(self):
200 200 """Close method.
201 201
202 202 Closes the underlying comm.
203 203 When the comm is closed, all of the widget views are automatically
204 204 removed from the front-end."""
205 205 if self.comm is not None:
206 206 Widget.widgets.pop(self.model_id, None)
207 207 self.comm.close()
208 208 self.comm = None
209 209
210 210 def send_state(self, key=None):
211 211 """Sends the widget state, or a piece of it, to the front-end.
212 212
213 213 Parameters
214 214 ----------
215 215 key : unicode, or iterable (optional)
216 216 A single property's name or iterable of property names to sync with the front-end.
217 217 """
218 218 self._send({
219 219 "method" : "update",
220 220 "state" : self.get_state(key=key)
221 221 })
222 222
223 223 def get_state(self, key=None):
224 224 """Gets the widget state, or a piece of it.
225 225
226 226 Parameters
227 227 ----------
228 228 key : unicode or iterable (optional)
229 229 A single property's name or iterable of property names to get.
230 230 """
231 231 if key is None:
232 232 keys = self.keys
233 233 elif isinstance(key, string_types):
234 234 keys = [key]
235 235 elif isinstance(key, collections.Iterable):
236 236 keys = key
237 237 else:
238 238 raise ValueError("key must be a string, an iterable of keys, or None")
239 239 state = {}
240 240 for k in keys:
241 241 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
242 242 value = getattr(self, k)
243 243 state[k] = f(value)
244 244 return state
245 245
246 246 def set_state(self, sync_data):
247 247 """Called when a state is received from the front-end."""
248 248 for name in self.keys:
249 249 if name in sync_data:
250 250 json_value = sync_data[name]
251 251 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
252 252 with self._lock_property(name, json_value):
253 253 setattr(self, name, from_json(json_value))
254 254
255 255 def send(self, content):
256 256 """Sends a custom msg to the widget model in the front-end.
257 257
258 258 Parameters
259 259 ----------
260 260 content : dict
261 261 Content of the message to send.
262 262 """
263 263 self._send({"method": "custom", "content": content})
264 264
265 265 def on_msg(self, callback, remove=False):
266 266 """(Un)Register a custom msg receive callback.
267 267
268 268 Parameters
269 269 ----------
270 270 callback: callable
271 271 callback will be passed two arguments when a message arrives::
272 272
273 273 callback(widget, content)
274 274
275 275 remove: bool
276 276 True if the callback should be unregistered."""
277 277 self._msg_callbacks.register_callback(callback, remove=remove)
278 278
279 279 def on_displayed(self, callback, remove=False):
280 280 """(Un)Register a widget displayed callback.
281 281
282 282 Parameters
283 283 ----------
284 284 callback: method handler
285 285 Must have a signature of::
286 286
287 287 callback(widget, **kwargs)
288 288
289 289 kwargs from display are passed through without modification.
290 290 remove: bool
291 291 True if the callback should be unregistered."""
292 292 self._display_callbacks.register_callback(callback, remove=remove)
293 293
294 294 #-------------------------------------------------------------------------
295 295 # Support methods
296 296 #-------------------------------------------------------------------------
297 297 @contextmanager
298 298 def _lock_property(self, key, value):
299 299 """Lock a property-value pair.
300 300
301 301 The value should be the JSON state of the property.
302 302
303 303 NOTE: This, in addition to the single lock for all state changes, is
304 304 flawed. In the future we may want to look into buffering state changes
305 305 back to the front-end."""
306 306 self._property_lock = (key, value)
307 307 try:
308 308 yield
309 309 finally:
310 310 self._property_lock = (None, None)
311 311
312 312 @contextmanager
313 313 def hold_sync(self):
314 314 """Hold syncing any state until the context manager is released"""
315 315 # We increment a value so that this can be nested. Syncing will happen when
316 316 # all levels have been released.
317 317 self._send_state_lock += 1
318 318 try:
319 319 yield
320 320 finally:
321 321 self._send_state_lock -=1
322 322 if self._send_state_lock == 0:
323 323 self.send_state(self._states_to_send)
324 324 self._states_to_send.clear()
325 325
326 326 def _should_send_property(self, key, value):
327 327 """Check the property lock (property_lock)"""
328 328 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
329 329 if (key == self._property_lock[0]
330 330 and to_json(value) == self._property_lock[1]):
331 331 return False
332 332 elif self._send_state_lock > 0:
333 333 self._states_to_send.add(key)
334 334 return False
335 335 else:
336 336 return True
337 337
338 338 # Event handlers
339 339 @_show_traceback
340 340 def _handle_msg(self, msg):
341 341 """Called when a msg is received from the front-end"""
342 342 data = msg['content']['data']
343 343 method = data['method']
344 if not method in ['backbone', 'custom']:
345 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
346 344
347 345 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
348 if method == 'backbone' and 'sync_data' in data:
349 sync_data = data['sync_data']
350 self.set_state(sync_data) # handles all methods
346 if method == 'backbone':
347 if 'sync_data' in data:
348 sync_data = data['sync_data']
349 self.set_state(sync_data) # handles all methods
350
351 # Handle a state request.
352 elif method == 'request_state':
353 self.send_state()
351 354
352 # Handle a custom msg from the front-end
355 # Handle a custom msg from the front-end.
353 356 elif method == 'custom':
354 357 if 'content' in data:
355 358 self._handle_custom_msg(data['content'])
356 359
360 # Catch remainder.
361 else:
362 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
363
357 364 def _handle_custom_msg(self, content):
358 365 """Called when a custom msg is received."""
359 366 self._msg_callbacks(self, content)
360 367
361 368 def _notify_trait(self, name, old_value, new_value):
362 369 """Called when a property has been changed."""
363 370 # Trigger default traitlet callback machinery. This allows any user
364 371 # registered validation to be processed prior to allowing the widget
365 372 # machinery to handle the state.
366 373 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
367 374
368 375 # Send the state after the user registered callbacks for trait changes
369 376 # have all fired (allows for user to validate values).
370 377 if self.comm is not None and name in self.keys:
371 # Make sure this isn't information that the front-end just sent us.
378 # Make sure this isn't information that the front-end just sent us.
372 379 if self._should_send_property(name, new_value):
373 380 # Send new state to front-end
374 381 self.send_state(key=name)
375 382
376 383 def _handle_displayed(self, **kwargs):
377 384 """Called when a view has been displayed for this widget instance"""
378 385 self._display_callbacks(self, **kwargs)
379 386
380 387 def _trait_to_json(self, x):
381 388 """Convert a trait value to json
382 389
383 390 Traverse lists/tuples and dicts and serialize their values as well.
384 391 Replace any widgets with their model_id
385 392 """
386 393 if isinstance(x, dict):
387 394 return {k: self._trait_to_json(v) for k, v in x.items()}
388 395 elif isinstance(x, (list, tuple)):
389 396 return [self._trait_to_json(v) for v in x]
390 397 elif isinstance(x, Widget):
391 398 return "IPY_MODEL_" + x.model_id
392 399 else:
393 400 return x # Value must be JSON-able
394 401
395 402 def _trait_from_json(self, x):
396 403 """Convert json values to objects
397 404
398 405 Replace any strings representing valid model id values to Widget references.
399 406 """
400 407 if isinstance(x, dict):
401 408 return {k: self._trait_from_json(v) for k, v in x.items()}
402 409 elif isinstance(x, (list, tuple)):
403 410 return [self._trait_from_json(v) for v in x]
404 411 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
405 412 # we want to support having child widgets at any level in a hierarchy
406 413 # trusting that a widget UUID will not appear out in the wild
407 414 return Widget.widgets[x[10:]]
408 415 else:
409 416 return x
410 417
411 418 def _ipython_display_(self, **kwargs):
412 419 """Called when `IPython.display.display` is called on the widget."""
413 420 # Show view.
414 421 if self._view_name is not None:
415 422 self._send({"method": "display"})
416 423 self._handle_displayed(**kwargs)
417 424
418 425 def _send(self, msg):
419 426 """Sends a message to the model in the front-end."""
420 427 self.comm.send(msg)
421 428
422 429
423 430 class DOMWidget(Widget):
424 431 visible = Bool(True, help="Whether the widget is visible.", sync=True)
425 432 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
426 433 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
427 434
428 435 width = CUnicode(sync=True)
429 436 height = CUnicode(sync=True)
430 437 padding = CUnicode(sync=True)
431 438 margin = CUnicode(sync=True)
432 439
433 440 color = Unicode(sync=True)
434 441 background_color = Unicode(sync=True)
435 442 border_color = Unicode(sync=True)
436 443
437 444 border_width = CUnicode(sync=True)
438 445 border_radius = CUnicode(sync=True)
439 446 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
440 447 'none',
441 448 'hidden',
442 449 'dotted',
443 450 'dashed',
444 451 'solid',
445 452 'double',
446 453 'groove',
447 454 'ridge',
448 455 'inset',
449 456 'outset',
450 457 'initial',
451 458 'inherit', ''],
452 459 default_value='', sync=True)
453 460
454 461 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
455 462 'normal',
456 463 'italic',
457 464 'oblique',
458 465 'initial',
459 466 'inherit', ''],
460 467 default_value='', sync=True)
461 468 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
462 469 'normal',
463 470 'bold',
464 471 'bolder',
465 472 'lighter',
466 473 'initial',
467 474 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
468 475 default_value='', sync=True)
469 476 font_size = CUnicode(sync=True)
470 477 font_family = Unicode(sync=True)
471 478
472 479 def __init__(self, *pargs, **kwargs):
473 480 super(DOMWidget, self).__init__(*pargs, **kwargs)
474 481
475 482 def _validate_border(name, old, new):
476 483 if new is not None and new != '':
477 484 if name != 'border_width' and not self.border_width:
478 485 self.border_width = 1
479 486 if name != 'border_style' and self.border_style == '':
480 487 self.border_style = 'solid'
481 488 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now