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