##// END OF EJS Templates
Removed ability to clear stdout and stderr individually.
Jonathan Frederic -
Show More
@@ -1,691 +1,671 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Top-level display functions for displaying object in different formats.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 from __future__ import print_function
21 21
22 22 import os
23 23 import struct
24 24
25 25 from IPython.utils.py3compat import string_types, cast_bytes_py2, cast_unicode
26 26
27 27 from .displaypub import publish_display_data
28 28
29 29 #-----------------------------------------------------------------------------
30 30 # utility functions
31 31 #-----------------------------------------------------------------------------
32 32
33 33 def _safe_exists(path):
34 34 """Check path, but don't let exceptions raise"""
35 35 try:
36 36 return os.path.exists(path)
37 37 except Exception:
38 38 return False
39 39
40 40 def _merge(d1, d2):
41 41 """Like update, but merges sub-dicts instead of clobbering at the top level.
42 42
43 43 Updates d1 in-place
44 44 """
45 45
46 46 if not isinstance(d2, dict) or not isinstance(d1, dict):
47 47 return d2
48 48 for key, value in d2.items():
49 49 d1[key] = _merge(d1.get(key), value)
50 50 return d1
51 51
52 52 def _display_mimetype(mimetype, objs, raw=False, metadata=None):
53 53 """internal implementation of all display_foo methods
54 54
55 55 Parameters
56 56 ----------
57 57 mimetype : str
58 58 The mimetype to be published (e.g. 'image/png')
59 59 objs : tuple of objects
60 60 The Python objects to display, or if raw=True raw text data to
61 61 display.
62 62 raw : bool
63 63 Are the data objects raw data or Python objects that need to be
64 64 formatted before display? [default: False]
65 65 metadata : dict (optional)
66 66 Metadata to be associated with the specific mimetype output.
67 67 """
68 68 if metadata:
69 69 metadata = {mimetype: metadata}
70 70 if raw:
71 71 # turn list of pngdata into list of { 'image/png': pngdata }
72 72 objs = [ {mimetype: obj} for obj in objs ]
73 73 display(*objs, raw=raw, metadata=metadata, include=[mimetype])
74 74
75 75 #-----------------------------------------------------------------------------
76 76 # Main functions
77 77 #-----------------------------------------------------------------------------
78 78
79 79 def display(*objs, **kwargs):
80 80 """Display a Python object in all frontends.
81 81
82 82 By default all representations will be computed and sent to the frontends.
83 83 Frontends can decide which representation is used and how.
84 84
85 85 Parameters
86 86 ----------
87 87 objs : tuple of objects
88 88 The Python objects to display.
89 89 raw : bool, optional
90 90 Are the objects to be displayed already mimetype-keyed dicts of raw display data,
91 91 or Python objects that need to be formatted before display? [default: False]
92 92 include : list or tuple, optional
93 93 A list of format type strings (MIME types) to include in the
94 94 format data dict. If this is set *only* the format types included
95 95 in this list will be computed.
96 96 exclude : list or tuple, optional
97 97 A list of format type strings (MIME types) to exclude in the format
98 98 data dict. If this is set all format types will be computed,
99 99 except for those included in this argument.
100 100 metadata : dict, optional
101 101 A dictionary of metadata to associate with the output.
102 102 mime-type keys in this dictionary will be associated with the individual
103 103 representation formats, if they exist.
104 104 """
105 105 raw = kwargs.get('raw', False)
106 106 include = kwargs.get('include')
107 107 exclude = kwargs.get('exclude')
108 108 metadata = kwargs.get('metadata')
109 109
110 110 from IPython.core.interactiveshell import InteractiveShell
111 111
112 112 if raw:
113 113 for obj in objs:
114 114 publish_display_data('display', obj, metadata)
115 115 else:
116 116 format = InteractiveShell.instance().display_formatter.format
117 117 for obj in objs:
118 118 format_dict, md_dict = format(obj, include=include, exclude=exclude)
119 119 if metadata:
120 120 # kwarg-specified metadata gets precedence
121 121 _merge(md_dict, metadata)
122 122 publish_display_data('display', format_dict, md_dict)
123 123
124 124
125 125 def display_pretty(*objs, **kwargs):
126 126 """Display the pretty (default) representation of an object.
127 127
128 128 Parameters
129 129 ----------
130 130 objs : tuple of objects
131 131 The Python objects to display, or if raw=True raw text data to
132 132 display.
133 133 raw : bool
134 134 Are the data objects raw data or Python objects that need to be
135 135 formatted before display? [default: False]
136 136 metadata : dict (optional)
137 137 Metadata to be associated with the specific mimetype output.
138 138 """
139 139 _display_mimetype('text/plain', objs, **kwargs)
140 140
141 141
142 142 def display_html(*objs, **kwargs):
143 143 """Display the HTML representation of an object.
144 144
145 145 Parameters
146 146 ----------
147 147 objs : tuple of objects
148 148 The Python objects to display, or if raw=True raw HTML data to
149 149 display.
150 150 raw : bool
151 151 Are the data objects raw data or Python objects that need to be
152 152 formatted before display? [default: False]
153 153 metadata : dict (optional)
154 154 Metadata to be associated with the specific mimetype output.
155 155 """
156 156 _display_mimetype('text/html', objs, **kwargs)
157 157
158 158
159 159 def display_svg(*objs, **kwargs):
160 160 """Display the SVG representation of an object.
161 161
162 162 Parameters
163 163 ----------
164 164 objs : tuple of objects
165 165 The Python objects to display, or if raw=True raw svg data to
166 166 display.
167 167 raw : bool
168 168 Are the data objects raw data or Python objects that need to be
169 169 formatted before display? [default: False]
170 170 metadata : dict (optional)
171 171 Metadata to be associated with the specific mimetype output.
172 172 """
173 173 _display_mimetype('image/svg+xml', objs, **kwargs)
174 174
175 175
176 176 def display_png(*objs, **kwargs):
177 177 """Display the PNG representation of an object.
178 178
179 179 Parameters
180 180 ----------
181 181 objs : tuple of objects
182 182 The Python objects to display, or if raw=True raw png data to
183 183 display.
184 184 raw : bool
185 185 Are the data objects raw data or Python objects that need to be
186 186 formatted before display? [default: False]
187 187 metadata : dict (optional)
188 188 Metadata to be associated with the specific mimetype output.
189 189 """
190 190 _display_mimetype('image/png', objs, **kwargs)
191 191
192 192
193 193 def display_jpeg(*objs, **kwargs):
194 194 """Display the JPEG representation of an object.
195 195
196 196 Parameters
197 197 ----------
198 198 objs : tuple of objects
199 199 The Python objects to display, or if raw=True raw JPEG data to
200 200 display.
201 201 raw : bool
202 202 Are the data objects raw data or Python objects that need to be
203 203 formatted before display? [default: False]
204 204 metadata : dict (optional)
205 205 Metadata to be associated with the specific mimetype output.
206 206 """
207 207 _display_mimetype('image/jpeg', objs, **kwargs)
208 208
209 209
210 210 def display_latex(*objs, **kwargs):
211 211 """Display the LaTeX representation of an object.
212 212
213 213 Parameters
214 214 ----------
215 215 objs : tuple of objects
216 216 The Python objects to display, or if raw=True raw latex data to
217 217 display.
218 218 raw : bool
219 219 Are the data objects raw data or Python objects that need to be
220 220 formatted before display? [default: False]
221 221 metadata : dict (optional)
222 222 Metadata to be associated with the specific mimetype output.
223 223 """
224 224 _display_mimetype('text/latex', objs, **kwargs)
225 225
226 226
227 227 def display_json(*objs, **kwargs):
228 228 """Display the JSON representation of an object.
229 229
230 230 Note that not many frontends support displaying JSON.
231 231
232 232 Parameters
233 233 ----------
234 234 objs : tuple of objects
235 235 The Python objects to display, or if raw=True raw json data to
236 236 display.
237 237 raw : bool
238 238 Are the data objects raw data or Python objects that need to be
239 239 formatted before display? [default: False]
240 240 metadata : dict (optional)
241 241 Metadata to be associated with the specific mimetype output.
242 242 """
243 243 _display_mimetype('application/json', objs, **kwargs)
244 244
245 245
246 246 def display_javascript(*objs, **kwargs):
247 247 """Display the Javascript representation of an object.
248 248
249 249 Parameters
250 250 ----------
251 251 objs : tuple of objects
252 252 The Python objects to display, or if raw=True raw javascript data to
253 253 display.
254 254 raw : bool
255 255 Are the data objects raw data or Python objects that need to be
256 256 formatted before display? [default: False]
257 257 metadata : dict (optional)
258 258 Metadata to be associated with the specific mimetype output.
259 259 """
260 260 _display_mimetype('application/javascript', objs, **kwargs)
261 261
262 262 #-----------------------------------------------------------------------------
263 263 # Smart classes
264 264 #-----------------------------------------------------------------------------
265 265
266 266
267 267 class DisplayObject(object):
268 268 """An object that wraps data to be displayed."""
269 269
270 270 _read_flags = 'r'
271 271
272 272 def __init__(self, data=None, url=None, filename=None):
273 273 """Create a display object given raw data.
274 274
275 275 When this object is returned by an expression or passed to the
276 276 display function, it will result in the data being displayed
277 277 in the frontend. The MIME type of the data should match the
278 278 subclasses used, so the Png subclass should be used for 'image/png'
279 279 data. If the data is a URL, the data will first be downloaded
280 280 and then displayed. If
281 281
282 282 Parameters
283 283 ----------
284 284 data : unicode, str or bytes
285 285 The raw data or a URL or file to load the data from
286 286 url : unicode
287 287 A URL to download the data from.
288 288 filename : unicode
289 289 Path to a local file to load the data from.
290 290 """
291 291 if data is not None and isinstance(data, string_types):
292 292 if data.startswith('http') and url is None:
293 293 url = data
294 294 filename = None
295 295 data = None
296 296 elif _safe_exists(data) and filename is None:
297 297 url = None
298 298 filename = data
299 299 data = None
300 300
301 301 self.data = data
302 302 self.url = url
303 303 self.filename = None if filename is None else unicode(filename)
304 304
305 305 self.reload()
306 306
307 307 def reload(self):
308 308 """Reload the raw data from file or URL."""
309 309 if self.filename is not None:
310 310 with open(self.filename, self._read_flags) as f:
311 311 self.data = f.read()
312 312 elif self.url is not None:
313 313 try:
314 314 import urllib2
315 315 response = urllib2.urlopen(self.url)
316 316 self.data = response.read()
317 317 # extract encoding from header, if there is one:
318 318 encoding = None
319 319 for sub in response.headers['content-type'].split(';'):
320 320 sub = sub.strip()
321 321 if sub.startswith('charset'):
322 322 encoding = sub.split('=')[-1].strip()
323 323 break
324 324 # decode data, if an encoding was specified
325 325 if encoding:
326 326 self.data = self.data.decode(encoding, 'replace')
327 327 except:
328 328 self.data = None
329 329
330 330 class Pretty(DisplayObject):
331 331
332 332 def _repr_pretty_(self):
333 333 return self.data
334 334
335 335
336 336 class HTML(DisplayObject):
337 337
338 338 def _repr_html_(self):
339 339 return self.data
340 340
341 341 def __html__(self):
342 342 """
343 343 This method exists to inform other HTML-using modules (e.g. Markupsafe,
344 344 htmltag, etc) that this object is HTML and does not need things like
345 345 special characters (<>&) escaped.
346 346 """
347 347 return self._repr_html_()
348 348
349 349
350 350 class Math(DisplayObject):
351 351
352 352 def _repr_latex_(self):
353 353 s = self.data.strip('$')
354 354 return "$$%s$$" % s
355 355
356 356
357 357 class Latex(DisplayObject):
358 358
359 359 def _repr_latex_(self):
360 360 return self.data
361 361
362 362
363 363 class SVG(DisplayObject):
364 364
365 365 # wrap data in a property, which extracts the <svg> tag, discarding
366 366 # document headers
367 367 _data = None
368 368
369 369 @property
370 370 def data(self):
371 371 return self._data
372 372
373 373 @data.setter
374 374 def data(self, svg):
375 375 if svg is None:
376 376 self._data = None
377 377 return
378 378 # parse into dom object
379 379 from xml.dom import minidom
380 380 svg = cast_bytes_py2(svg)
381 381 x = minidom.parseString(svg)
382 382 # get svg tag (should be 1)
383 383 found_svg = x.getElementsByTagName('svg')
384 384 if found_svg:
385 385 svg = found_svg[0].toxml()
386 386 else:
387 387 # fallback on the input, trust the user
388 388 # but this is probably an error.
389 389 pass
390 390 svg = cast_unicode(svg)
391 391 self._data = svg
392 392
393 393 def _repr_svg_(self):
394 394 return self.data
395 395
396 396
397 397 class JSON(DisplayObject):
398 398
399 399 def _repr_json_(self):
400 400 return self.data
401 401
402 402 css_t = """$("head").append($("<link/>").attr({
403 403 rel: "stylesheet",
404 404 type: "text/css",
405 405 href: "%s"
406 406 }));
407 407 """
408 408
409 409 lib_t1 = """$.getScript("%s", function () {
410 410 """
411 411 lib_t2 = """});
412 412 """
413 413
414 414 class Javascript(DisplayObject):
415 415
416 416 def __init__(self, data=None, url=None, filename=None, lib=None, css=None):
417 417 """Create a Javascript display object given raw data.
418 418
419 419 When this object is returned by an expression or passed to the
420 420 display function, it will result in the data being displayed
421 421 in the frontend. If the data is a URL, the data will first be
422 422 downloaded and then displayed.
423 423
424 424 In the Notebook, the containing element will be available as `element`,
425 425 and jQuery will be available. The output area starts hidden, so if
426 426 the js appends content to `element` that should be visible, then
427 427 it must call `container.show()` to unhide the area.
428 428
429 429 Parameters
430 430 ----------
431 431 data : unicode, str or bytes
432 432 The Javascript source code or a URL to download it from.
433 433 url : unicode
434 434 A URL to download the data from.
435 435 filename : unicode
436 436 Path to a local file to load the data from.
437 437 lib : list or str
438 438 A sequence of Javascript library URLs to load asynchronously before
439 439 running the source code. The full URLs of the libraries should
440 440 be given. A single Javascript library URL can also be given as a
441 441 string.
442 442 css: : list or str
443 443 A sequence of css files to load before running the source code.
444 444 The full URLs of the css files should be given. A single css URL
445 445 can also be given as a string.
446 446 """
447 447 if isinstance(lib, basestring):
448 448 lib = [lib]
449 449 elif lib is None:
450 450 lib = []
451 451 if isinstance(css, basestring):
452 452 css = [css]
453 453 elif css is None:
454 454 css = []
455 455 if not isinstance(lib, (list,tuple)):
456 456 raise TypeError('expected sequence, got: %r' % lib)
457 457 if not isinstance(css, (list,tuple)):
458 458 raise TypeError('expected sequence, got: %r' % css)
459 459 self.lib = lib
460 460 self.css = css
461 461 super(Javascript, self).__init__(data=data, url=url, filename=filename)
462 462
463 463 def _repr_javascript_(self):
464 464 r = ''
465 465 for c in self.css:
466 466 r += css_t % c
467 467 for l in self.lib:
468 468 r += lib_t1 % l
469 469 r += self.data
470 470 r += lib_t2*len(self.lib)
471 471 return r
472 472
473 473 # constants for identifying png/jpeg data
474 474 _PNG = b'\x89PNG\r\n\x1a\n'
475 475 _JPEG = b'\xff\xd8'
476 476
477 477 def _pngxy(data):
478 478 """read the (width, height) from a PNG header"""
479 479 ihdr = data.index(b'IHDR')
480 480 # next 8 bytes are width/height
481 481 w4h4 = data[ihdr+4:ihdr+12]
482 482 return struct.unpack('>ii', w4h4)
483 483
484 484 def _jpegxy(data):
485 485 """read the (width, height) from a JPEG header"""
486 486 # adapted from http://www.64lines.com/jpeg-width-height
487 487
488 488 idx = 4
489 489 while True:
490 490 block_size = struct.unpack('>H', data[idx:idx+2])[0]
491 491 idx = idx + block_size
492 492 if data[idx:idx+2] == b'\xFF\xC0':
493 493 # found Start of Frame
494 494 iSOF = idx
495 495 break
496 496 else:
497 497 # read another block
498 498 idx += 2
499 499
500 500 h, w = struct.unpack('>HH', data[iSOF+5:iSOF+9])
501 501 return w, h
502 502
503 503 class Image(DisplayObject):
504 504
505 505 _read_flags = 'rb'
506 506 _FMT_JPEG = u'jpeg'
507 507 _FMT_PNG = u'png'
508 508 _ACCEPTABLE_EMBEDDINGS = [_FMT_JPEG, _FMT_PNG]
509 509
510 510 def __init__(self, data=None, url=None, filename=None, format=u'png', embed=None, width=None, height=None, retina=False):
511 511 """Create a PNG/JPEG image object given raw data.
512 512
513 513 When this object is returned by an input cell or passed to the
514 514 display function, it will result in the image being displayed
515 515 in the frontend.
516 516
517 517 Parameters
518 518 ----------
519 519 data : unicode, str or bytes
520 520 The raw image data or a URL or filename to load the data from.
521 521 This always results in embedded image data.
522 522 url : unicode
523 523 A URL to download the data from. If you specify `url=`,
524 524 the image data will not be embedded unless you also specify `embed=True`.
525 525 filename : unicode
526 526 Path to a local file to load the data from.
527 527 Images from a file are always embedded.
528 528 format : unicode
529 529 The format of the image data (png/jpeg/jpg). If a filename or URL is given
530 530 for format will be inferred from the filename extension.
531 531 embed : bool
532 532 Should the image data be embedded using a data URI (True) or be
533 533 loaded using an <img> tag. Set this to True if you want the image
534 534 to be viewable later with no internet connection in the notebook.
535 535
536 536 Default is `True`, unless the keyword argument `url` is set, then
537 537 default value is `False`.
538 538
539 539 Note that QtConsole is not able to display images if `embed` is set to `False`
540 540 width : int
541 541 Width to which to constrain the image in html
542 542 height : int
543 543 Height to which to constrain the image in html
544 544 retina : bool
545 545 Automatically set the width and height to half of the measured
546 546 width and height.
547 547 This only works for embedded images because it reads the width/height
548 548 from image data.
549 549 For non-embedded images, you can just set the desired display width
550 550 and height directly.
551 551
552 552 Examples
553 553 --------
554 554 # embedded image data, works in qtconsole and notebook
555 555 # when passed positionally, the first arg can be any of raw image data,
556 556 # a URL, or a filename from which to load image data.
557 557 # The result is always embedding image data for inline images.
558 558 Image('http://www.google.fr/images/srpr/logo3w.png')
559 559 Image('/path/to/image.jpg')
560 560 Image(b'RAW_PNG_DATA...')
561 561
562 562 # Specifying Image(url=...) does not embed the image data,
563 563 # it only generates `<img>` tag with a link to the source.
564 564 # This will not work in the qtconsole or offline.
565 565 Image(url='http://www.google.fr/images/srpr/logo3w.png')
566 566
567 567 """
568 568 if filename is not None:
569 569 ext = self._find_ext(filename)
570 570 elif url is not None:
571 571 ext = self._find_ext(url)
572 572 elif data is None:
573 573 raise ValueError("No image data found. Expecting filename, url, or data.")
574 574 elif isinstance(data, string_types) and (
575 575 data.startswith('http') or _safe_exists(data)
576 576 ):
577 577 ext = self._find_ext(data)
578 578 else:
579 579 ext = None
580 580
581 581 if ext is not None:
582 582 format = ext.lower()
583 583 if ext == u'jpg' or ext == u'jpeg':
584 584 format = self._FMT_JPEG
585 585 if ext == u'png':
586 586 format = self._FMT_PNG
587 587 elif isinstance(data, bytes) and format == 'png':
588 588 # infer image type from image data header,
589 589 # only if format might not have been specified.
590 590 if data[:2] == _JPEG:
591 591 format = 'jpeg'
592 592
593 593 self.format = unicode(format).lower()
594 594 self.embed = embed if embed is not None else (url is None)
595 595
596 596 if self.embed and self.format not in self._ACCEPTABLE_EMBEDDINGS:
597 597 raise ValueError("Cannot embed the '%s' image format" % (self.format))
598 598 self.width = width
599 599 self.height = height
600 600 self.retina = retina
601 601 super(Image, self).__init__(data=data, url=url, filename=filename)
602 602
603 603 if retina:
604 604 self._retina_shape()
605 605
606 606 def _retina_shape(self):
607 607 """load pixel-doubled width and height from image data"""
608 608 if not self.embed:
609 609 return
610 610 if self.format == 'png':
611 611 w, h = _pngxy(self.data)
612 612 elif self.format == 'jpeg':
613 613 w, h = _jpegxy(self.data)
614 614 else:
615 615 # retina only supports png
616 616 return
617 617 self.width = w // 2
618 618 self.height = h // 2
619 619
620 620 def reload(self):
621 621 """Reload the raw data from file or URL."""
622 622 if self.embed:
623 623 super(Image,self).reload()
624 624 if self.retina:
625 625 self._retina_shape()
626 626
627 627 def _repr_html_(self):
628 628 if not self.embed:
629 629 width = height = ''
630 630 if self.width:
631 631 width = ' width="%d"' % self.width
632 632 if self.height:
633 633 height = ' height="%d"' % self.height
634 634 return u'<img src="%s"%s%s/>' % (self.url, width, height)
635 635
636 636 def _data_and_metadata(self):
637 637 """shortcut for returning metadata with shape information, if defined"""
638 638 md = {}
639 639 if self.width:
640 640 md['width'] = self.width
641 641 if self.height:
642 642 md['height'] = self.height
643 643 if md:
644 644 return self.data, md
645 645 else:
646 646 return self.data
647 647
648 648 def _repr_png_(self):
649 649 if self.embed and self.format == u'png':
650 650 return self._data_and_metadata()
651 651
652 652 def _repr_jpeg_(self):
653 653 if self.embed and (self.format == u'jpeg' or self.format == u'jpg'):
654 654 return self._data_and_metadata()
655 655
656 656 def _find_ext(self, s):
657 657 return unicode(s.split('.')[-1].lower())
658 658
659 659
660 def clear_output(stdout=True, stderr=True, other=True):
661 """Clear the output of the current cell receiving output.
662
663 Optionally, each of stdout/stderr or other non-stream data (e.g. anything
664 produced by display()) can be excluded from the clear event.
665
666 By default, everything is cleared.
667
668 Parameters
669 ----------
670 stdout : bool [default: True]
671 Whether to clear stdout.
672 stderr : bool [default: True]
673 Whether to clear stderr.
674 other : bool [default: True]
675 Whether to clear everything else that is not stdout/stderr
676 (e.g. figures,images,HTML, any result of display()).
677 """
660 def clear_output():
661 """Clear the output of the current cell receiving output."""
678 662 from IPython.core.interactiveshell import InteractiveShell
679 663 if InteractiveShell.initialized():
680 InteractiveShell.instance().display_pub.clear_output(
681 stdout=stdout, stderr=stderr, other=other,
682 )
664 InteractiveShell.instance().display_pub.clear_output()
683 665 else:
684 666 from IPython.utils import io
685 if stdout:
686 print('\033[2K\r', file=io.stdout, end='')
687 io.stdout.flush()
688 if stderr:
689 print('\033[2K\r', file=io.stderr, end='')
690 io.stderr.flush()
667 print('\033[2K\r', file=io.stdout, end='')
668 io.stdout.flush()
669 print('\033[2K\r', file=io.stderr, end='')
670 io.stderr.flush()
691 671
@@ -1,178 +1,176 b''
1 1 """An interface for publishing rich data to frontends.
2 2
3 3 There are two components of the display system:
4 4
5 5 * Display formatters, which take a Python object and compute the
6 6 representation of the object in various formats (text, HTML, SVG, etc.).
7 7 * The display publisher that is used to send the representation data to the
8 8 various frontends.
9 9
10 10 This module defines the logic display publishing. The display publisher uses
11 11 the ``display_data`` message type that is defined in the IPython messaging
12 12 spec.
13 13
14 14 Authors:
15 15
16 16 * Brian Granger
17 17 """
18 18
19 19 #-----------------------------------------------------------------------------
20 20 # Copyright (C) 2008-2011 The IPython Development Team
21 21 #
22 22 # Distributed under the terms of the BSD License. The full license is in
23 23 # the file COPYING, distributed as part of this software.
24 24 #-----------------------------------------------------------------------------
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Imports
28 28 #-----------------------------------------------------------------------------
29 29
30 30 from __future__ import print_function
31 31
32 32 from IPython.config.configurable import Configurable
33 33 from IPython.utils import io
34 34 from IPython.utils.traitlets import List
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Main payload class
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class DisplayPublisher(Configurable):
41 41 """A traited class that publishes display data to frontends.
42 42
43 43 Instances of this class are created by the main IPython object and should
44 44 be accessed there.
45 45 """
46 46
47 47 def _validate_data(self, source, data, metadata=None):
48 48 """Validate the display data.
49 49
50 50 Parameters
51 51 ----------
52 52 source : str
53 53 The fully dotted name of the callable that created the data, like
54 54 :func:`foo.bar.my_formatter`.
55 55 data : dict
56 56 The formata data dictionary.
57 57 metadata : dict
58 58 Any metadata for the data.
59 59 """
60 60
61 61 if not isinstance(source, basestring):
62 62 raise TypeError('source must be a str, got: %r' % source)
63 63 if not isinstance(data, dict):
64 64 raise TypeError('data must be a dict, got: %r' % data)
65 65 if metadata is not None:
66 66 if not isinstance(metadata, dict):
67 67 raise TypeError('metadata must be a dict, got: %r' % data)
68 68
69 69 def publish(self, source, data, metadata=None):
70 70 """Publish data and metadata to all frontends.
71 71
72 72 See the ``display_data`` message in the messaging documentation for
73 73 more details about this message type.
74 74
75 75 The following MIME types are currently implemented:
76 76
77 77 * text/plain
78 78 * text/html
79 79 * text/latex
80 80 * application/json
81 81 * application/javascript
82 82 * image/png
83 83 * image/jpeg
84 84 * image/svg+xml
85 85
86 86 Parameters
87 87 ----------
88 88 source : str
89 89 A string that give the function or method that created the data,
90 90 such as 'IPython.core.page'.
91 91 data : dict
92 92 A dictionary having keys that are valid MIME types (like
93 93 'text/plain' or 'image/svg+xml') and values that are the data for
94 94 that MIME type. The data itself must be a JSON'able data
95 95 structure. Minimally all data should have the 'text/plain' data,
96 96 which can be displayed by all frontends. If more than the plain
97 97 text is given, it is up to the frontend to decide which
98 98 representation to use.
99 99 metadata : dict
100 100 A dictionary for metadata related to the data. This can contain
101 101 arbitrary key, value pairs that frontends can use to interpret
102 102 the data. Metadata specific to each mime-type can be specified
103 103 in the metadata dict with the same mime-type keys as
104 104 the data itself.
105 105 """
106 106
107 107 # The default is to simply write the plain text data using io.stdout.
108 108 if 'text/plain' in data:
109 109 print(data['text/plain'], file=io.stdout)
110 110
111 def clear_output(self, stdout=True, stderr=True, other=True):
111 def clear_output(self):
112 112 """Clear the output of the cell receiving output."""
113 if stdout:
114 print('\033[2K\r', file=io.stdout, end='')
115 io.stdout.flush()
116 if stderr:
117 print('\033[2K\r', file=io.stderr, end='')
118 io.stderr.flush()
113 print('\033[2K\r', file=io.stdout, end='')
114 io.stdout.flush()
115 print('\033[2K\r', file=io.stderr, end='')
116 io.stderr.flush()
119 117
120 118
121 119 class CapturingDisplayPublisher(DisplayPublisher):
122 120 """A DisplayPublisher that stores"""
123 121 outputs = List()
124 122
125 123 def publish(self, source, data, metadata=None):
126 124 self.outputs.append((source, data, metadata))
127 125
128 def clear_output(self, stdout=True, stderr=True, other=True):
129 super(CapturingDisplayPublisher, self).clear_output(stdout, stderr, other)
126 def clear_output(self):
127 super(CapturingDisplayPublisher, self).clear_output()
130 128 if other:
131 129 # empty the list, *do not* reassign a new list
132 130 del self.outputs[:]
133 131
134 132
135 133 def publish_display_data(source, data, metadata=None):
136 134 """Publish data and metadata to all frontends.
137 135
138 136 See the ``display_data`` message in the messaging documentation for
139 137 more details about this message type.
140 138
141 139 The following MIME types are currently implemented:
142 140
143 141 * text/plain
144 142 * text/html
145 143 * text/latex
146 144 * application/json
147 145 * application/javascript
148 146 * image/png
149 147 * image/jpeg
150 148 * image/svg+xml
151 149
152 150 Parameters
153 151 ----------
154 152 source : str
155 153 A string that give the function or method that created the data,
156 154 such as 'IPython.core.page'.
157 155 data : dict
158 156 A dictionary having keys that are valid MIME types (like
159 157 'text/plain' or 'image/svg+xml') and values that are the data for
160 158 that MIME type. The data itself must be a JSON'able data
161 159 structure. Minimally all data should have the 'text/plain' data,
162 160 which can be displayed by all frontends. If more than the plain
163 161 text is given, it is up to the frontend to decide which
164 162 representation to use.
165 163 metadata : dict
166 164 A dictionary for metadata related to the data. This can contain
167 165 arbitrary key, value pairs that frontends can use to interpret
168 166 the data. mime-type keys matching those in data can be used
169 167 to specify metadata about particular representations.
170 168 """
171 169 from IPython.core.interactiveshell import InteractiveShell
172 170 InteractiveShell.instance().display_pub.publish(
173 171 source,
174 172 data,
175 173 metadata
176 174 )
177 175
178 176
@@ -1,441 +1,441 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // CodeCell
10 10 //============================================================================
11 11 /**
12 12 * An extendable module that provide base functionnality to create cell for notebook.
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule CodeCell
16 16 */
17 17
18 18
19 19 /* local util for codemirror */
20 20 var posEq = function(a, b) {return a.line == b.line && a.ch == b.ch;}
21 21
22 22 /**
23 23 *
24 24 * function to delete until previous non blanking space character
25 25 * or first multiple of 4 tabstop.
26 26 * @private
27 27 */
28 28 CodeMirror.commands.delSpaceToPrevTabStop = function(cm){
29 29 var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
30 30 if (!posEq(from, to)) {cm.replaceRange("", from, to); return}
31 31 var cur = cm.getCursor(), line = cm.getLine(cur.line);
32 32 var tabsize = cm.getOption('tabSize');
33 33 var chToPrevTabStop = cur.ch-(Math.ceil(cur.ch/tabsize)-1)*tabsize;
34 34 var from = {ch:cur.ch-chToPrevTabStop,line:cur.line}
35 35 var select = cm.getRange(from,cur)
36 36 if( select.match(/^\ +$/) != null){
37 37 cm.replaceRange("",from,cur)
38 38 } else {
39 39 cm.deleteH(-1,"char")
40 40 }
41 41 };
42 42
43 43
44 44 var IPython = (function (IPython) {
45 45 "use strict";
46 46
47 47 var utils = IPython.utils;
48 48 var key = IPython.utils.keycodes;
49 49
50 50 /**
51 51 * A Cell conceived to write code.
52 52 *
53 53 * The kernel doesn't have to be set at creation time, in that case
54 54 * it will be null and set_kernel has to be called later.
55 55 * @class CodeCell
56 56 * @extends IPython.Cell
57 57 *
58 58 * @constructor
59 59 * @param {Object|null} kernel
60 60 * @param {object|undefined} [options]
61 61 * @param [options.cm_config] {object} config to pass to CodeMirror
62 62 */
63 63 var CodeCell = function (kernel, options) {
64 64 this.kernel = kernel || null;
65 65 this.code_mirror = null;
66 66 this.input_prompt_number = null;
67 67 this.collapsed = false;
68 68 this.cell_type = "code";
69 69
70 70
71 71 var cm_overwrite_options = {
72 72 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
73 73 };
74 74
75 75 options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options});
76 76
77 77 IPython.Cell.apply(this,[options]);
78 78
79 79 var that = this;
80 80 this.element.focusout(
81 81 function() { that.auto_highlight(); }
82 82 );
83 83 };
84 84
85 85 CodeCell.options_default = {
86 86 cm_config : {
87 87 extraKeys: {
88 88 "Tab" : "indentMore",
89 89 "Shift-Tab" : "indentLess",
90 90 "Backspace" : "delSpaceToPrevTabStop",
91 91 "Cmd-/" : "toggleComment",
92 92 "Ctrl-/" : "toggleComment"
93 93 },
94 94 mode: 'ipython',
95 95 theme: 'ipython',
96 96 matchBrackets: true
97 97 }
98 98 };
99 99
100 100
101 101 CodeCell.prototype = new IPython.Cell();
102 102
103 103 /**
104 104 * @method auto_highlight
105 105 */
106 106 CodeCell.prototype.auto_highlight = function () {
107 107 this._auto_highlight(IPython.config.cell_magic_highlight)
108 108 };
109 109
110 110 /** @method create_element */
111 111 CodeCell.prototype.create_element = function () {
112 112 IPython.Cell.prototype.create_element.apply(this, arguments);
113 113
114 114 var cell = $('<div></div>').addClass('cell border-box-sizing code_cell');
115 115 cell.attr('tabindex','2');
116 116
117 117 this.celltoolbar = new IPython.CellToolbar(this);
118 118
119 119 var input = $('<div></div>').addClass('input');
120 120 var vbox = $('<div/>').addClass('vbox box-flex1')
121 121 input.append($('<div/>').addClass('prompt input_prompt'));
122 122 vbox.append(this.celltoolbar.element);
123 123 var input_area = $('<div/>').addClass('input_area');
124 124 this.code_mirror = CodeMirror(input_area.get(0), this.cm_config);
125 125 $(this.code_mirror.getInputField()).attr("spellcheck", "false");
126 126 vbox.append(input_area);
127 127 input.append(vbox);
128 128 var output = $('<div></div>');
129 129 cell.append(input).append(output);
130 130 this.element = cell;
131 131 this.output_area = new IPython.OutputArea(output, true);
132 132
133 133 // construct a completer only if class exist
134 134 // otherwise no print view
135 135 if (IPython.Completer !== undefined)
136 136 {
137 137 this.completer = new IPython.Completer(this);
138 138 }
139 139 };
140 140
141 141 /**
142 142 * This method gets called in CodeMirror's onKeyDown/onKeyPress
143 143 * handlers and is used to provide custom key handling. Its return
144 144 * value is used to determine if CodeMirror should ignore the event:
145 145 * true = ignore, false = don't ignore.
146 146 * @method handle_codemirror_keyevent
147 147 */
148 148 CodeCell.prototype.handle_codemirror_keyevent = function (editor, event) {
149 149
150 150 var that = this;
151 151 // whatever key is pressed, first, cancel the tooltip request before
152 152 // they are sent, and remove tooltip if any, except for tab again
153 153 if (event.type === 'keydown' && event.which != key.TAB ) {
154 154 IPython.tooltip.remove_and_cancel_tooltip();
155 155 };
156 156
157 157 var cur = editor.getCursor();
158 158 if (event.keyCode === key.ENTER){
159 159 this.auto_highlight();
160 160 }
161 161
162 162 if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey)) {
163 163 // Always ignore shift-enter in CodeMirror as we handle it.
164 164 return true;
165 165 } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) {
166 166 // triger on keypress (!) otherwise inconsistent event.which depending on plateform
167 167 // browser and keyboard layout !
168 168 // Pressing '(' , request tooltip, don't forget to reappend it
169 169 // The second argument says to hide the tooltip if the docstring
170 170 // is actually empty
171 171 IPython.tooltip.pending(that, true);
172 172 } else if (event.which === key.UPARROW && event.type === 'keydown') {
173 173 // If we are not at the top, let CM handle the up arrow and
174 174 // prevent the global keydown handler from handling it.
175 175 if (!that.at_top()) {
176 176 event.stop();
177 177 return false;
178 178 } else {
179 179 return true;
180 180 };
181 181 } else if (event.which === key.ESC) {
182 182 return IPython.tooltip.remove_and_cancel_tooltip(true);
183 183 } else if (event.which === key.DOWNARROW && event.type === 'keydown') {
184 184 // If we are not at the bottom, let CM handle the down arrow and
185 185 // prevent the global keydown handler from handling it.
186 186 if (!that.at_bottom()) {
187 187 event.stop();
188 188 return false;
189 189 } else {
190 190 return true;
191 191 };
192 192 } else if (event.keyCode === key.TAB && event.type == 'keydown' && event.shiftKey) {
193 193 if (editor.somethingSelected()){
194 194 var anchor = editor.getCursor("anchor");
195 195 var head = editor.getCursor("head");
196 196 if( anchor.line != head.line){
197 197 return false;
198 198 }
199 199 }
200 200 IPython.tooltip.request(that);
201 201 event.stop();
202 202 return true;
203 203 } else if (event.keyCode === key.TAB && event.type == 'keydown') {
204 204 // Tab completion.
205 205 //Do not trim here because of tooltip
206 206 if (editor.somethingSelected()){return false}
207 207 var pre_cursor = editor.getRange({line:cur.line,ch:0},cur);
208 208 if (pre_cursor.trim() === "") {
209 209 // Don't autocomplete if the part of the line before the cursor
210 210 // is empty. In this case, let CodeMirror handle indentation.
211 211 return false;
212 212 } else if ((pre_cursor.substr(-1) === "("|| pre_cursor.substr(-1) === " ") && IPython.config.tooltip_on_tab ) {
213 213 IPython.tooltip.request(that);
214 214 // Prevent the event from bubbling up.
215 215 event.stop();
216 216 // Prevent CodeMirror from handling the tab.
217 217 return true;
218 218 } else {
219 219 event.stop();
220 220 this.completer.startCompletion();
221 221 return true;
222 222 };
223 223 } else {
224 224 // keypress/keyup also trigger on TAB press, and we don't want to
225 225 // use those to disable tab completion.
226 226 return false;
227 227 };
228 228 return false;
229 229 };
230 230
231 231
232 232 // Kernel related calls.
233 233
234 234 CodeCell.prototype.set_kernel = function (kernel) {
235 235 this.kernel = kernel;
236 236 }
237 237
238 238 /**
239 239 * Execute current code cell to the kernel
240 240 * @method execute
241 241 */
242 242 CodeCell.prototype.execute = function () {
243 this.output_area.clear_output(true, true, true);
243 this.output_area.clear_output();
244 244 this.set_input_prompt('*');
245 245 this.element.addClass("running");
246 246 var callbacks = {
247 247 'execute_reply': $.proxy(this._handle_execute_reply, this),
248 248 'output': $.proxy(this.output_area.handle_output, this.output_area),
249 249 'clear_output': $.proxy(this.output_area.handle_clear_output, this.output_area),
250 250 'set_next_input': $.proxy(this._handle_set_next_input, this),
251 251 'input_request': $.proxy(this._handle_input_request, this)
252 252 };
253 253 var msg_id = this.kernel.execute(this.get_text(), callbacks, {silent: false, store_history: true});
254 254 };
255 255
256 256 /**
257 257 * @method _handle_execute_reply
258 258 * @private
259 259 */
260 260 CodeCell.prototype._handle_execute_reply = function (content) {
261 261 this.set_input_prompt(content.execution_count);
262 262 this.element.removeClass("running");
263 263 $([IPython.events]).trigger('set_dirty.Notebook', {value: true});
264 264 }
265 265
266 266 /**
267 267 * @method _handle_set_next_input
268 268 * @private
269 269 */
270 270 CodeCell.prototype._handle_set_next_input = function (text) {
271 271 var data = {'cell': this, 'text': text}
272 272 $([IPython.events]).trigger('set_next_input.Notebook', data);
273 273 }
274 274
275 275 /**
276 276 * @method _handle_input_request
277 277 * @private
278 278 */
279 279 CodeCell.prototype._handle_input_request = function (content) {
280 280 this.output_area.append_raw_input(content);
281 281 }
282 282
283 283
284 284 // Basic cell manipulation.
285 285
286 286 CodeCell.prototype.select = function () {
287 287 IPython.Cell.prototype.select.apply(this);
288 288 this.code_mirror.refresh();
289 289 this.code_mirror.focus();
290 290 this.auto_highlight();
291 291 // We used to need an additional refresh() after the focus, but
292 292 // it appears that this has been fixed in CM. This bug would show
293 293 // up on FF when a newly loaded markdown cell was edited.
294 294 };
295 295
296 296
297 297 CodeCell.prototype.select_all = function () {
298 298 var start = {line: 0, ch: 0};
299 299 var nlines = this.code_mirror.lineCount();
300 300 var last_line = this.code_mirror.getLine(nlines-1);
301 301 var end = {line: nlines-1, ch: last_line.length};
302 302 this.code_mirror.setSelection(start, end);
303 303 };
304 304
305 305
306 306 CodeCell.prototype.collapse = function () {
307 307 this.collapsed = true;
308 308 this.output_area.collapse();
309 309 };
310 310
311 311
312 312 CodeCell.prototype.expand = function () {
313 313 this.collapsed = false;
314 314 this.output_area.expand();
315 315 };
316 316
317 317
318 318 CodeCell.prototype.toggle_output = function () {
319 319 this.collapsed = Boolean(1 - this.collapsed);
320 320 this.output_area.toggle_output();
321 321 };
322 322
323 323
324 324 CodeCell.prototype.toggle_output_scroll = function () {
325 325 this.output_area.toggle_scroll();
326 326 };
327 327
328 328
329 329 CodeCell.input_prompt_classical = function (prompt_value, lines_number) {
330 330 var ns = prompt_value || "&nbsp;";
331 331 return 'In&nbsp;[' + ns + ']:'
332 332 };
333 333
334 334 CodeCell.input_prompt_continuation = function (prompt_value, lines_number) {
335 335 var html = [CodeCell.input_prompt_classical(prompt_value, lines_number)];
336 336 for(var i=1; i < lines_number; i++){html.push(['...:'])};
337 337 return html.join('</br>')
338 338 };
339 339
340 340 CodeCell.input_prompt_function = CodeCell.input_prompt_classical;
341 341
342 342
343 343 CodeCell.prototype.set_input_prompt = function (number) {
344 344 var nline = 1
345 345 if( this.code_mirror != undefined) {
346 346 nline = this.code_mirror.lineCount();
347 347 }
348 348 this.input_prompt_number = number;
349 349 var prompt_html = CodeCell.input_prompt_function(this.input_prompt_number, nline);
350 350 this.element.find('div.input_prompt').html(prompt_html);
351 351 };
352 352
353 353
354 354 CodeCell.prototype.clear_input = function () {
355 355 this.code_mirror.setValue('');
356 356 };
357 357
358 358
359 359 CodeCell.prototype.get_text = function () {
360 360 return this.code_mirror.getValue();
361 361 };
362 362
363 363
364 364 CodeCell.prototype.set_text = function (code) {
365 365 return this.code_mirror.setValue(code);
366 366 };
367 367
368 368
369 369 CodeCell.prototype.at_top = function () {
370 370 var cursor = this.code_mirror.getCursor();
371 371 if (cursor.line === 0 && cursor.ch === 0) {
372 372 return true;
373 373 } else {
374 374 return false;
375 375 }
376 376 };
377 377
378 378
379 379 CodeCell.prototype.at_bottom = function () {
380 380 var cursor = this.code_mirror.getCursor();
381 381 if (cursor.line === (this.code_mirror.lineCount()-1) && cursor.ch === this.code_mirror.getLine(cursor.line).length) {
382 382 return true;
383 383 } else {
384 384 return false;
385 385 }
386 386 };
387 387
388 388
389 CodeCell.prototype.clear_output = function (stdout, stderr, other) {
390 this.output_area.clear_output(stdout, stderr, other);
389 CodeCell.prototype.clear_output = function () {
390 this.output_area.clear_output();
391 391 };
392 392
393 393
394 394 // JSON serialization
395 395
396 396 CodeCell.prototype.fromJSON = function (data) {
397 397 IPython.Cell.prototype.fromJSON.apply(this, arguments);
398 398 if (data.cell_type === 'code') {
399 399 if (data.input !== undefined) {
400 400 this.set_text(data.input);
401 401 // make this value the starting point, so that we can only undo
402 402 // to this state, instead of a blank cell
403 403 this.code_mirror.clearHistory();
404 404 this.auto_highlight();
405 405 }
406 406 if (data.prompt_number !== undefined) {
407 407 this.set_input_prompt(data.prompt_number);
408 408 } else {
409 409 this.set_input_prompt();
410 410 };
411 411 this.output_area.fromJSON(data.outputs);
412 412 if (data.collapsed !== undefined) {
413 413 if (data.collapsed) {
414 414 this.collapse();
415 415 } else {
416 416 this.expand();
417 417 };
418 418 };
419 419 };
420 420 };
421 421
422 422
423 423 CodeCell.prototype.toJSON = function () {
424 424 var data = IPython.Cell.prototype.toJSON.apply(this);
425 425 data.input = this.get_text();
426 426 data.cell_type = 'code';
427 427 if (this.input_prompt_number) {
428 428 data.prompt_number = this.input_prompt_number;
429 429 };
430 430 var outputs = this.output_area.toJSON();
431 431 data.outputs = outputs;
432 432 data.language = 'python';
433 433 data.collapsed = this.collapsed;
434 434 return data;
435 435 };
436 436
437 437
438 438 IPython.CodeCell = CodeCell;
439 439
440 440 return IPython;
441 441 }(IPython));
@@ -1,2077 +1,2077 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2011 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // Notebook
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 var utils = IPython.utils;
16 16 var key = IPython.utils.keycodes;
17 17
18 18 /**
19 19 * A notebook contains and manages cells.
20 20 *
21 21 * @class Notebook
22 22 * @constructor
23 23 * @param {String} selector A jQuery selector for the notebook's DOM element
24 24 * @param {Object} [options] A config object
25 25 */
26 26 var Notebook = function (selector, options) {
27 27 var options = options || {};
28 28 this._baseProjectUrl = options.baseProjectUrl;
29 29
30 30 this.element = $(selector);
31 31 this.element.scroll();
32 32 this.element.data("notebook", this);
33 33 this.next_prompt_number = 1;
34 34 this.kernel = null;
35 35 this.clipboard = null;
36 36 this.undelete_backup = null;
37 37 this.undelete_index = null;
38 38 this.undelete_below = false;
39 39 this.paste_enabled = false;
40 40 this.set_dirty(false);
41 41 this.metadata = {};
42 42 this._checkpoint_after_save = false;
43 43 this.last_checkpoint = null;
44 44 this.checkpoints = [];
45 45 this.autosave_interval = 0;
46 46 this.autosave_timer = null;
47 47 // autosave *at most* every two minutes
48 48 this.minimum_autosave_interval = 120000;
49 49 // single worksheet for now
50 50 this.worksheet_metadata = {};
51 51 this.control_key_active = false;
52 52 this.notebook_id = null;
53 53 this.notebook_name = null;
54 54 this.notebook_name_blacklist_re = /[\/\\:]/;
55 55 this.nbformat = 3 // Increment this when changing the nbformat
56 56 this.nbformat_minor = 0 // Increment this when changing the nbformat
57 57 this.style();
58 58 this.create_elements();
59 59 this.bind_events();
60 60 };
61 61
62 62 /**
63 63 * Tweak the notebook's CSS style.
64 64 *
65 65 * @method style
66 66 */
67 67 Notebook.prototype.style = function () {
68 68 $('div#notebook').addClass('border-box-sizing');
69 69 };
70 70
71 71 /**
72 72 * Get the root URL of the notebook server.
73 73 *
74 74 * @method baseProjectUrl
75 75 * @return {String} The base project URL
76 76 */
77 77 Notebook.prototype.baseProjectUrl = function(){
78 78 return this._baseProjectUrl || $('body').data('baseProjectUrl');
79 79 };
80 80
81 81 /**
82 82 * Create an HTML and CSS representation of the notebook.
83 83 *
84 84 * @method create_elements
85 85 */
86 86 Notebook.prototype.create_elements = function () {
87 87 // We add this end_space div to the end of the notebook div to:
88 88 // i) provide a margin between the last cell and the end of the notebook
89 89 // ii) to prevent the div from scrolling up when the last cell is being
90 90 // edited, but is too low on the page, which browsers will do automatically.
91 91 var that = this;
92 92 this.container = $("<div/>").addClass("container").attr("id", "notebook-container");
93 93 var end_space = $('<div/>').addClass('end_space');
94 94 end_space.dblclick(function (e) {
95 95 var ncells = that.ncells();
96 96 that.insert_cell_below('code',ncells-1);
97 97 });
98 98 this.element.append(this.container);
99 99 this.container.append(end_space);
100 100 $('div#notebook').addClass('border-box-sizing');
101 101 };
102 102
103 103 /**
104 104 * Bind JavaScript events: key presses and custom IPython events.
105 105 *
106 106 * @method bind_events
107 107 */
108 108 Notebook.prototype.bind_events = function () {
109 109 var that = this;
110 110
111 111 $([IPython.events]).on('set_next_input.Notebook', function (event, data) {
112 112 var index = that.find_cell_index(data.cell);
113 113 var new_cell = that.insert_cell_below('code',index);
114 114 new_cell.set_text(data.text);
115 115 that.dirty = true;
116 116 });
117 117
118 118 $([IPython.events]).on('set_dirty.Notebook', function (event, data) {
119 119 that.dirty = data.value;
120 120 });
121 121
122 122 $([IPython.events]).on('select.Cell', function (event, data) {
123 123 var index = that.find_cell_index(data.cell);
124 124 that.select(index);
125 125 });
126 126
127 127 $([IPython.events]).on('status_autorestarting.Kernel', function () {
128 128 IPython.dialog.modal({
129 129 title: "Kernel Restarting",
130 130 body: "The kernel appears to have died. It will restart automatically.",
131 131 buttons: {
132 132 OK : {
133 133 class : "btn-primary"
134 134 }
135 135 }
136 136 });
137 137 });
138 138
139 139
140 140 $(document).keydown(function (event) {
141 141
142 142 // Save (CTRL+S) or (AppleKey+S)
143 143 //metaKey = applekey on mac
144 144 if ((event.ctrlKey || event.metaKey) && event.keyCode==83) {
145 145 that.save_checkpoint();
146 146 event.preventDefault();
147 147 return false;
148 148 } else if (event.which === key.ESC) {
149 149 // Intercept escape at highest level to avoid closing
150 150 // websocket connection with firefox
151 151 IPython.pager.collapse();
152 152 event.preventDefault();
153 153 } else if (event.which === key.SHIFT) {
154 154 // ignore shift keydown
155 155 return true;
156 156 }
157 157 if (event.which === key.UPARROW && !event.shiftKey) {
158 158 var cell = that.get_selected_cell();
159 159 if (cell && cell.at_top()) {
160 160 event.preventDefault();
161 161 that.select_prev();
162 162 };
163 163 } else if (event.which === key.DOWNARROW && !event.shiftKey) {
164 164 var cell = that.get_selected_cell();
165 165 if (cell && cell.at_bottom()) {
166 166 event.preventDefault();
167 167 that.select_next();
168 168 };
169 169 } else if (event.which === key.ENTER && event.shiftKey) {
170 170 that.execute_selected_cell();
171 171 return false;
172 172 } else if (event.which === key.ENTER && event.altKey) {
173 173 // Execute code cell, and insert new in place
174 174 that.execute_selected_cell();
175 175 // Only insert a new cell, if we ended up in an already populated cell
176 176 if (/\S/.test(that.get_selected_cell().get_text()) == true) {
177 177 that.insert_cell_above('code');
178 178 }
179 179 return false;
180 180 } else if (event.which === key.ENTER && event.ctrlKey) {
181 181 that.execute_selected_cell({terminal:true});
182 182 return false;
183 183 } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) {
184 184 that.control_key_active = true;
185 185 return false;
186 186 } else if (event.which === 88 && that.control_key_active) {
187 187 // Cut selected cell = x
188 188 that.cut_cell();
189 189 that.control_key_active = false;
190 190 return false;
191 191 } else if (event.which === 67 && that.control_key_active) {
192 192 // Copy selected cell = c
193 193 that.copy_cell();
194 194 that.control_key_active = false;
195 195 return false;
196 196 } else if (event.which === 86 && that.control_key_active) {
197 197 // Paste below selected cell = v
198 198 that.paste_cell_below();
199 199 that.control_key_active = false;
200 200 return false;
201 201 } else if (event.which === 68 && that.control_key_active) {
202 202 // Delete selected cell = d
203 203 that.delete_cell();
204 204 that.control_key_active = false;
205 205 return false;
206 206 } else if (event.which === 65 && that.control_key_active) {
207 207 // Insert code cell above selected = a
208 208 that.insert_cell_above('code');
209 209 that.control_key_active = false;
210 210 return false;
211 211 } else if (event.which === 66 && that.control_key_active) {
212 212 // Insert code cell below selected = b
213 213 that.insert_cell_below('code');
214 214 that.control_key_active = false;
215 215 return false;
216 216 } else if (event.which === 89 && that.control_key_active) {
217 217 // To code = y
218 218 that.to_code();
219 219 that.control_key_active = false;
220 220 return false;
221 221 } else if (event.which === 77 && that.control_key_active) {
222 222 // To markdown = m
223 223 that.to_markdown();
224 224 that.control_key_active = false;
225 225 return false;
226 226 } else if (event.which === 84 && that.control_key_active) {
227 227 // To Raw = t
228 228 that.to_raw();
229 229 that.control_key_active = false;
230 230 return false;
231 231 } else if (event.which === 49 && that.control_key_active) {
232 232 // To Heading 1 = 1
233 233 that.to_heading(undefined, 1);
234 234 that.control_key_active = false;
235 235 return false;
236 236 } else if (event.which === 50 && that.control_key_active) {
237 237 // To Heading 2 = 2
238 238 that.to_heading(undefined, 2);
239 239 that.control_key_active = false;
240 240 return false;
241 241 } else if (event.which === 51 && that.control_key_active) {
242 242 // To Heading 3 = 3
243 243 that.to_heading(undefined, 3);
244 244 that.control_key_active = false;
245 245 return false;
246 246 } else if (event.which === 52 && that.control_key_active) {
247 247 // To Heading 4 = 4
248 248 that.to_heading(undefined, 4);
249 249 that.control_key_active = false;
250 250 return false;
251 251 } else if (event.which === 53 && that.control_key_active) {
252 252 // To Heading 5 = 5
253 253 that.to_heading(undefined, 5);
254 254 that.control_key_active = false;
255 255 return false;
256 256 } else if (event.which === 54 && that.control_key_active) {
257 257 // To Heading 6 = 6
258 258 that.to_heading(undefined, 6);
259 259 that.control_key_active = false;
260 260 return false;
261 261 } else if (event.which === 79 && that.control_key_active) {
262 262 // Toggle output = o
263 263 if (event.shiftKey){
264 264 that.toggle_output_scroll();
265 265 } else {
266 266 that.toggle_output();
267 267 }
268 268 that.control_key_active = false;
269 269 return false;
270 270 } else if (event.which === 83 && that.control_key_active) {
271 271 // Save notebook = s
272 272 that.save_checkpoint();
273 273 that.control_key_active = false;
274 274 return false;
275 275 } else if (event.which === 74 && that.control_key_active) {
276 276 // Move cell down = j
277 277 that.move_cell_down();
278 278 that.control_key_active = false;
279 279 return false;
280 280 } else if (event.which === 75 && that.control_key_active) {
281 281 // Move cell up = k
282 282 that.move_cell_up();
283 283 that.control_key_active = false;
284 284 return false;
285 285 } else if (event.which === 80 && that.control_key_active) {
286 286 // Select previous = p
287 287 that.select_prev();
288 288 that.control_key_active = false;
289 289 return false;
290 290 } else if (event.which === 78 && that.control_key_active) {
291 291 // Select next = n
292 292 that.select_next();
293 293 that.control_key_active = false;
294 294 return false;
295 295 } else if (event.which === 76 && that.control_key_active) {
296 296 // Toggle line numbers = l
297 297 that.cell_toggle_line_numbers();
298 298 that.control_key_active = false;
299 299 return false;
300 300 } else if (event.which === 73 && that.control_key_active) {
301 301 // Interrupt kernel = i
302 302 that.kernel.interrupt();
303 303 that.control_key_active = false;
304 304 return false;
305 305 } else if (event.which === 190 && that.control_key_active) {
306 306 // Restart kernel = . # matches qt console
307 307 that.restart_kernel();
308 308 that.control_key_active = false;
309 309 return false;
310 310 } else if (event.which === 72 && that.control_key_active) {
311 311 // Show keyboard shortcuts = h
312 312 IPython.quick_help.show_keyboard_shortcuts();
313 313 that.control_key_active = false;
314 314 return false;
315 315 } else if (event.which === 90 && that.control_key_active) {
316 316 // Undo last cell delete = z
317 317 that.undelete();
318 318 that.control_key_active = false;
319 319 return false;
320 320 } else if (event.which === 189 && that.control_key_active) {
321 321 // Split cell = -
322 322 that.split_cell();
323 323 that.control_key_active = false;
324 324 return false;
325 325 } else if (that.control_key_active) {
326 326 that.control_key_active = false;
327 327 return true;
328 328 }
329 329 return true;
330 330 });
331 331
332 332 var collapse_time = function(time){
333 333 var app_height = $('#ipython-main-app').height(); // content height
334 334 var splitter_height = $('div#pager_splitter').outerHeight(true);
335 335 var new_height = app_height - splitter_height;
336 336 that.element.animate({height : new_height + 'px'}, time);
337 337 }
338 338
339 339 this.element.bind('collapse_pager', function (event,extrap) {
340 340 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
341 341 collapse_time(time);
342 342 });
343 343
344 344 var expand_time = function(time) {
345 345 var app_height = $('#ipython-main-app').height(); // content height
346 346 var splitter_height = $('div#pager_splitter').outerHeight(true);
347 347 var pager_height = $('div#pager').outerHeight(true);
348 348 var new_height = app_height - pager_height - splitter_height;
349 349 that.element.animate({height : new_height + 'px'}, time);
350 350 }
351 351
352 352 this.element.bind('expand_pager', function (event, extrap) {
353 353 var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast';
354 354 expand_time(time);
355 355 });
356 356
357 357 // Firefox 22 broke $(window).on("beforeunload")
358 358 // I'm not sure why or how.
359 359 window.onbeforeunload = function (e) {
360 360 // TODO: Make killing the kernel configurable.
361 361 var kill_kernel = false;
362 362 if (kill_kernel) {
363 363 that.kernel.kill();
364 364 }
365 365 // if we are autosaving, trigger an autosave on nav-away.
366 366 // still warn, because if we don't the autosave may fail.
367 367 if (that.dirty) {
368 368 if ( that.autosave_interval ) {
369 369 // schedule autosave in a timeout
370 370 // this gives you a chance to forcefully discard changes
371 371 // by reloading the page if you *really* want to.
372 372 // the timer doesn't start until you *dismiss* the dialog.
373 373 setTimeout(function () {
374 374 if (that.dirty) {
375 375 that.save_notebook();
376 376 }
377 377 }, 1000);
378 378 return "Autosave in progress, latest changes may be lost.";
379 379 } else {
380 380 return "Unsaved changes will be lost.";
381 381 }
382 382 };
383 383 // Null is the *only* return value that will make the browser not
384 384 // pop up the "don't leave" dialog.
385 385 return null;
386 386 };
387 387 };
388 388
389 389 /**
390 390 * Set the dirty flag, and trigger the set_dirty.Notebook event
391 391 *
392 392 * @method set_dirty
393 393 */
394 394 Notebook.prototype.set_dirty = function (value) {
395 395 if (value === undefined) {
396 396 value = true;
397 397 }
398 398 if (this.dirty == value) {
399 399 return;
400 400 }
401 401 $([IPython.events]).trigger('set_dirty.Notebook', {value: value});
402 402 };
403 403
404 404 /**
405 405 * Scroll the top of the page to a given cell.
406 406 *
407 407 * @method scroll_to_cell
408 408 * @param {Number} cell_number An index of the cell to view
409 409 * @param {Number} time Animation time in milliseconds
410 410 * @return {Number} Pixel offset from the top of the container
411 411 */
412 412 Notebook.prototype.scroll_to_cell = function (cell_number, time) {
413 413 var cells = this.get_cells();
414 414 var time = time || 0;
415 415 cell_number = Math.min(cells.length-1,cell_number);
416 416 cell_number = Math.max(0 ,cell_number);
417 417 var scroll_value = cells[cell_number].element.position().top-cells[0].element.position().top ;
418 418 this.element.animate({scrollTop:scroll_value}, time);
419 419 return scroll_value;
420 420 };
421 421
422 422 /**
423 423 * Scroll to the bottom of the page.
424 424 *
425 425 * @method scroll_to_bottom
426 426 */
427 427 Notebook.prototype.scroll_to_bottom = function () {
428 428 this.element.animate({scrollTop:this.element.get(0).scrollHeight}, 0);
429 429 };
430 430
431 431 /**
432 432 * Scroll to the top of the page.
433 433 *
434 434 * @method scroll_to_top
435 435 */
436 436 Notebook.prototype.scroll_to_top = function () {
437 437 this.element.animate({scrollTop:0}, 0);
438 438 };
439 439
440 440
441 441 // Cell indexing, retrieval, etc.
442 442
443 443 /**
444 444 * Get all cell elements in the notebook.
445 445 *
446 446 * @method get_cell_elements
447 447 * @return {jQuery} A selector of all cell elements
448 448 */
449 449 Notebook.prototype.get_cell_elements = function () {
450 450 return this.container.children("div.cell");
451 451 };
452 452
453 453 /**
454 454 * Get a particular cell element.
455 455 *
456 456 * @method get_cell_element
457 457 * @param {Number} index An index of a cell to select
458 458 * @return {jQuery} A selector of the given cell.
459 459 */
460 460 Notebook.prototype.get_cell_element = function (index) {
461 461 var result = null;
462 462 var e = this.get_cell_elements().eq(index);
463 463 if (e.length !== 0) {
464 464 result = e;
465 465 }
466 466 return result;
467 467 };
468 468
469 469 /**
470 470 * Count the cells in this notebook.
471 471 *
472 472 * @method ncells
473 473 * @return {Number} The number of cells in this notebook
474 474 */
475 475 Notebook.prototype.ncells = function () {
476 476 return this.get_cell_elements().length;
477 477 };
478 478
479 479 /**
480 480 * Get all Cell objects in this notebook.
481 481 *
482 482 * @method get_cells
483 483 * @return {Array} This notebook's Cell objects
484 484 */
485 485 // TODO: we are often calling cells as cells()[i], which we should optimize
486 486 // to cells(i) or a new method.
487 487 Notebook.prototype.get_cells = function () {
488 488 return this.get_cell_elements().toArray().map(function (e) {
489 489 return $(e).data("cell");
490 490 });
491 491 };
492 492
493 493 /**
494 494 * Get a Cell object from this notebook.
495 495 *
496 496 * @method get_cell
497 497 * @param {Number} index An index of a cell to retrieve
498 498 * @return {Cell} A particular cell
499 499 */
500 500 Notebook.prototype.get_cell = function (index) {
501 501 var result = null;
502 502 var ce = this.get_cell_element(index);
503 503 if (ce !== null) {
504 504 result = ce.data('cell');
505 505 }
506 506 return result;
507 507 }
508 508
509 509 /**
510 510 * Get the cell below a given cell.
511 511 *
512 512 * @method get_next_cell
513 513 * @param {Cell} cell The provided cell
514 514 * @return {Cell} The next cell
515 515 */
516 516 Notebook.prototype.get_next_cell = function (cell) {
517 517 var result = null;
518 518 var index = this.find_cell_index(cell);
519 519 if (this.is_valid_cell_index(index+1)) {
520 520 result = this.get_cell(index+1);
521 521 }
522 522 return result;
523 523 }
524 524
525 525 /**
526 526 * Get the cell above a given cell.
527 527 *
528 528 * @method get_prev_cell
529 529 * @param {Cell} cell The provided cell
530 530 * @return {Cell} The previous cell
531 531 */
532 532 Notebook.prototype.get_prev_cell = function (cell) {
533 533 // TODO: off-by-one
534 534 // nb.get_prev_cell(nb.get_cell(1)) is null
535 535 var result = null;
536 536 var index = this.find_cell_index(cell);
537 537 if (index !== null && index > 1) {
538 538 result = this.get_cell(index-1);
539 539 }
540 540 return result;
541 541 }
542 542
543 543 /**
544 544 * Get the numeric index of a given cell.
545 545 *
546 546 * @method find_cell_index
547 547 * @param {Cell} cell The provided cell
548 548 * @return {Number} The cell's numeric index
549 549 */
550 550 Notebook.prototype.find_cell_index = function (cell) {
551 551 var result = null;
552 552 this.get_cell_elements().filter(function (index) {
553 553 if ($(this).data("cell") === cell) {
554 554 result = index;
555 555 };
556 556 });
557 557 return result;
558 558 };
559 559
560 560 /**
561 561 * Get a given index , or the selected index if none is provided.
562 562 *
563 563 * @method index_or_selected
564 564 * @param {Number} index A cell's index
565 565 * @return {Number} The given index, or selected index if none is provided.
566 566 */
567 567 Notebook.prototype.index_or_selected = function (index) {
568 568 var i;
569 569 if (index === undefined || index === null) {
570 570 i = this.get_selected_index();
571 571 if (i === null) {
572 572 i = 0;
573 573 }
574 574 } else {
575 575 i = index;
576 576 }
577 577 return i;
578 578 };
579 579
580 580 /**
581 581 * Get the currently selected cell.
582 582 * @method get_selected_cell
583 583 * @return {Cell} The selected cell
584 584 */
585 585 Notebook.prototype.get_selected_cell = function () {
586 586 var index = this.get_selected_index();
587 587 return this.get_cell(index);
588 588 };
589 589
590 590 /**
591 591 * Check whether a cell index is valid.
592 592 *
593 593 * @method is_valid_cell_index
594 594 * @param {Number} index A cell index
595 595 * @return True if the index is valid, false otherwise
596 596 */
597 597 Notebook.prototype.is_valid_cell_index = function (index) {
598 598 if (index !== null && index >= 0 && index < this.ncells()) {
599 599 return true;
600 600 } else {
601 601 return false;
602 602 };
603 603 }
604 604
605 605 /**
606 606 * Get the index of the currently selected cell.
607 607
608 608 * @method get_selected_index
609 609 * @return {Number} The selected cell's numeric index
610 610 */
611 611 Notebook.prototype.get_selected_index = function () {
612 612 var result = null;
613 613 this.get_cell_elements().filter(function (index) {
614 614 if ($(this).data("cell").selected === true) {
615 615 result = index;
616 616 };
617 617 });
618 618 return result;
619 619 };
620 620
621 621
622 622 // Cell selection.
623 623
624 624 /**
625 625 * Programmatically select a cell.
626 626 *
627 627 * @method select
628 628 * @param {Number} index A cell's index
629 629 * @return {Notebook} This notebook
630 630 */
631 631 Notebook.prototype.select = function (index) {
632 632 if (this.is_valid_cell_index(index)) {
633 633 var sindex = this.get_selected_index()
634 634 if (sindex !== null && index !== sindex) {
635 635 this.get_cell(sindex).unselect();
636 636 };
637 637 var cell = this.get_cell(index);
638 638 cell.select();
639 639 if (cell.cell_type === 'heading') {
640 640 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
641 641 {'cell_type':cell.cell_type,level:cell.level}
642 642 );
643 643 } else {
644 644 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
645 645 {'cell_type':cell.cell_type}
646 646 );
647 647 };
648 648 };
649 649 return this;
650 650 };
651 651
652 652 /**
653 653 * Programmatically select the next cell.
654 654 *
655 655 * @method select_next
656 656 * @return {Notebook} This notebook
657 657 */
658 658 Notebook.prototype.select_next = function () {
659 659 var index = this.get_selected_index();
660 660 this.select(index+1);
661 661 return this;
662 662 };
663 663
664 664 /**
665 665 * Programmatically select the previous cell.
666 666 *
667 667 * @method select_prev
668 668 * @return {Notebook} This notebook
669 669 */
670 670 Notebook.prototype.select_prev = function () {
671 671 var index = this.get_selected_index();
672 672 this.select(index-1);
673 673 return this;
674 674 };
675 675
676 676
677 677 // Cell movement
678 678
679 679 /**
680 680 * Move given (or selected) cell up and select it.
681 681 *
682 682 * @method move_cell_up
683 683 * @param [index] {integer} cell index
684 684 * @return {Notebook} This notebook
685 685 **/
686 686 Notebook.prototype.move_cell_up = function (index) {
687 687 var i = this.index_or_selected(index);
688 688 if (this.is_valid_cell_index(i) && i > 0) {
689 689 var pivot = this.get_cell_element(i-1);
690 690 var tomove = this.get_cell_element(i);
691 691 if (pivot !== null && tomove !== null) {
692 692 tomove.detach();
693 693 pivot.before(tomove);
694 694 this.select(i-1);
695 695 };
696 696 this.set_dirty(true);
697 697 };
698 698 return this;
699 699 };
700 700
701 701
702 702 /**
703 703 * Move given (or selected) cell down and select it
704 704 *
705 705 * @method move_cell_down
706 706 * @param [index] {integer} cell index
707 707 * @return {Notebook} This notebook
708 708 **/
709 709 Notebook.prototype.move_cell_down = function (index) {
710 710 var i = this.index_or_selected(index);
711 711 if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) {
712 712 var pivot = this.get_cell_element(i+1);
713 713 var tomove = this.get_cell_element(i);
714 714 if (pivot !== null && tomove !== null) {
715 715 tomove.detach();
716 716 pivot.after(tomove);
717 717 this.select(i+1);
718 718 };
719 719 };
720 720 this.set_dirty();
721 721 return this;
722 722 };
723 723
724 724
725 725 // Insertion, deletion.
726 726
727 727 /**
728 728 * Delete a cell from the notebook.
729 729 *
730 730 * @method delete_cell
731 731 * @param [index] A cell's numeric index
732 732 * @return {Notebook} This notebook
733 733 */
734 734 Notebook.prototype.delete_cell = function (index) {
735 735 var i = this.index_or_selected(index);
736 736 var cell = this.get_selected_cell();
737 737 this.undelete_backup = cell.toJSON();
738 738 $('#undelete_cell').removeClass('disabled');
739 739 if (this.is_valid_cell_index(i)) {
740 740 var ce = this.get_cell_element(i);
741 741 ce.remove();
742 742 if (i === (this.ncells())) {
743 743 this.select(i-1);
744 744 this.undelete_index = i - 1;
745 745 this.undelete_below = true;
746 746 } else {
747 747 this.select(i);
748 748 this.undelete_index = i;
749 749 this.undelete_below = false;
750 750 };
751 751 $([IPython.events]).trigger('delete.Cell', {'cell': cell, 'index': i});
752 752 this.set_dirty(true);
753 753 };
754 754 return this;
755 755 };
756 756
757 757 /**
758 758 * Insert a cell so that after insertion the cell is at given index.
759 759 *
760 760 * Similar to insert_above, but index parameter is mandatory
761 761 *
762 762 * Index will be brought back into the accissible range [0,n]
763 763 *
764 764 * @method insert_cell_at_index
765 765 * @param type {string} in ['code','markdown','heading']
766 766 * @param [index] {int} a valid index where to inser cell
767 767 *
768 768 * @return cell {cell|null} created cell or null
769 769 **/
770 770 Notebook.prototype.insert_cell_at_index = function(type, index){
771 771
772 772 var ncells = this.ncells();
773 773 var index = Math.min(index,ncells);
774 774 index = Math.max(index,0);
775 775 var cell = null;
776 776
777 777 if (ncells === 0 || this.is_valid_cell_index(index) || index === ncells) {
778 778 if (type === 'code') {
779 779 cell = new IPython.CodeCell(this.kernel);
780 780 cell.set_input_prompt();
781 781 } else if (type === 'markdown') {
782 782 cell = new IPython.MarkdownCell();
783 783 } else if (type === 'raw') {
784 784 cell = new IPython.RawCell();
785 785 } else if (type === 'heading') {
786 786 cell = new IPython.HeadingCell();
787 787 }
788 788
789 789 if(this._insert_element_at_index(cell.element,index)){
790 790 cell.render();
791 791 this.select(this.find_cell_index(cell));
792 792 $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index});
793 793 this.set_dirty(true);
794 794 }
795 795 }
796 796 return cell;
797 797
798 798 };
799 799
800 800 /**
801 801 * Insert an element at given cell index.
802 802 *
803 803 * @method _insert_element_at_index
804 804 * @param element {dom element} a cell element
805 805 * @param [index] {int} a valid index where to inser cell
806 806 * @private
807 807 *
808 808 * return true if everything whent fine.
809 809 **/
810 810 Notebook.prototype._insert_element_at_index = function(element, index){
811 811 if (element === undefined){
812 812 return false;
813 813 }
814 814
815 815 var ncells = this.ncells();
816 816
817 817 if (ncells === 0) {
818 818 // special case append if empty
819 819 this.element.find('div.end_space').before(element);
820 820 } else if ( ncells === index ) {
821 821 // special case append it the end, but not empty
822 822 this.get_cell_element(index-1).after(element);
823 823 } else if (this.is_valid_cell_index(index)) {
824 824 // otherwise always somewhere to append to
825 825 this.get_cell_element(index).before(element);
826 826 } else {
827 827 return false;
828 828 }
829 829
830 830 if (this.undelete_index !== null && index <= this.undelete_index) {
831 831 this.undelete_index = this.undelete_index + 1;
832 832 this.set_dirty(true);
833 833 }
834 834 return true;
835 835 };
836 836
837 837 /**
838 838 * Insert a cell of given type above given index, or at top
839 839 * of notebook if index smaller than 0.
840 840 *
841 841 * default index value is the one of currently selected cell
842 842 *
843 843 * @method insert_cell_above
844 844 * @param type {string} cell type
845 845 * @param [index] {integer}
846 846 *
847 847 * @return handle to created cell or null
848 848 **/
849 849 Notebook.prototype.insert_cell_above = function (type, index) {
850 850 index = this.index_or_selected(index);
851 851 return this.insert_cell_at_index(type, index);
852 852 };
853 853
854 854 /**
855 855 * Insert a cell of given type below given index, or at bottom
856 856 * of notebook if index greater thatn number of cell
857 857 *
858 858 * default index value is the one of currently selected cell
859 859 *
860 860 * @method insert_cell_below
861 861 * @param type {string} cell type
862 862 * @param [index] {integer}
863 863 *
864 864 * @return handle to created cell or null
865 865 *
866 866 **/
867 867 Notebook.prototype.insert_cell_below = function (type, index) {
868 868 index = this.index_or_selected(index);
869 869 return this.insert_cell_at_index(type, index+1);
870 870 };
871 871
872 872
873 873 /**
874 874 * Insert cell at end of notebook
875 875 *
876 876 * @method insert_cell_at_bottom
877 877 * @param {String} type cell type
878 878 *
879 879 * @return the added cell; or null
880 880 **/
881 881 Notebook.prototype.insert_cell_at_bottom = function (type){
882 882 var len = this.ncells();
883 883 return this.insert_cell_below(type,len-1);
884 884 };
885 885
886 886 /**
887 887 * Turn a cell into a code cell.
888 888 *
889 889 * @method to_code
890 890 * @param {Number} [index] A cell's index
891 891 */
892 892 Notebook.prototype.to_code = function (index) {
893 893 var i = this.index_or_selected(index);
894 894 if (this.is_valid_cell_index(i)) {
895 895 var source_element = this.get_cell_element(i);
896 896 var source_cell = source_element.data("cell");
897 897 if (!(source_cell instanceof IPython.CodeCell)) {
898 898 var target_cell = this.insert_cell_below('code',i);
899 899 var text = source_cell.get_text();
900 900 if (text === source_cell.placeholder) {
901 901 text = '';
902 902 }
903 903 target_cell.set_text(text);
904 904 // make this value the starting point, so that we can only undo
905 905 // to this state, instead of a blank cell
906 906 target_cell.code_mirror.clearHistory();
907 907 source_element.remove();
908 908 this.set_dirty(true);
909 909 };
910 910 };
911 911 };
912 912
913 913 /**
914 914 * Turn a cell into a Markdown cell.
915 915 *
916 916 * @method to_markdown
917 917 * @param {Number} [index] A cell's index
918 918 */
919 919 Notebook.prototype.to_markdown = function (index) {
920 920 var i = this.index_or_selected(index);
921 921 if (this.is_valid_cell_index(i)) {
922 922 var source_element = this.get_cell_element(i);
923 923 var source_cell = source_element.data("cell");
924 924 if (!(source_cell instanceof IPython.MarkdownCell)) {
925 925 var target_cell = this.insert_cell_below('markdown',i);
926 926 var text = source_cell.get_text();
927 927 if (text === source_cell.placeholder) {
928 928 text = '';
929 929 };
930 930 // The edit must come before the set_text.
931 931 target_cell.edit();
932 932 target_cell.set_text(text);
933 933 // make this value the starting point, so that we can only undo
934 934 // to this state, instead of a blank cell
935 935 target_cell.code_mirror.clearHistory();
936 936 source_element.remove();
937 937 this.set_dirty(true);
938 938 };
939 939 };
940 940 };
941 941
942 942 /**
943 943 * Turn a cell into a raw text cell.
944 944 *
945 945 * @method to_raw
946 946 * @param {Number} [index] A cell's index
947 947 */
948 948 Notebook.prototype.to_raw = function (index) {
949 949 var i = this.index_or_selected(index);
950 950 if (this.is_valid_cell_index(i)) {
951 951 var source_element = this.get_cell_element(i);
952 952 var source_cell = source_element.data("cell");
953 953 var target_cell = null;
954 954 if (!(source_cell instanceof IPython.RawCell)) {
955 955 target_cell = this.insert_cell_below('raw',i);
956 956 var text = source_cell.get_text();
957 957 if (text === source_cell.placeholder) {
958 958 text = '';
959 959 };
960 960 // The edit must come before the set_text.
961 961 target_cell.edit();
962 962 target_cell.set_text(text);
963 963 // make this value the starting point, so that we can only undo
964 964 // to this state, instead of a blank cell
965 965 target_cell.code_mirror.clearHistory();
966 966 source_element.remove();
967 967 this.set_dirty(true);
968 968 };
969 969 };
970 970 };
971 971
972 972 /**
973 973 * Turn a cell into a heading cell.
974 974 *
975 975 * @method to_heading
976 976 * @param {Number} [index] A cell's index
977 977 * @param {Number} [level] A heading level (e.g., 1 becomes &lt;h1&gt;)
978 978 */
979 979 Notebook.prototype.to_heading = function (index, level) {
980 980 level = level || 1;
981 981 var i = this.index_or_selected(index);
982 982 if (this.is_valid_cell_index(i)) {
983 983 var source_element = this.get_cell_element(i);
984 984 var source_cell = source_element.data("cell");
985 985 var target_cell = null;
986 986 if (source_cell instanceof IPython.HeadingCell) {
987 987 source_cell.set_level(level);
988 988 } else {
989 989 target_cell = this.insert_cell_below('heading',i);
990 990 var text = source_cell.get_text();
991 991 if (text === source_cell.placeholder) {
992 992 text = '';
993 993 };
994 994 // The edit must come before the set_text.
995 995 target_cell.set_level(level);
996 996 target_cell.edit();
997 997 target_cell.set_text(text);
998 998 // make this value the starting point, so that we can only undo
999 999 // to this state, instead of a blank cell
1000 1000 target_cell.code_mirror.clearHistory();
1001 1001 source_element.remove();
1002 1002 this.set_dirty(true);
1003 1003 };
1004 1004 $([IPython.events]).trigger('selected_cell_type_changed.Notebook',
1005 1005 {'cell_type':'heading',level:level}
1006 1006 );
1007 1007 };
1008 1008 };
1009 1009
1010 1010
1011 1011 // Cut/Copy/Paste
1012 1012
1013 1013 /**
1014 1014 * Enable UI elements for pasting cells.
1015 1015 *
1016 1016 * @method enable_paste
1017 1017 */
1018 1018 Notebook.prototype.enable_paste = function () {
1019 1019 var that = this;
1020 1020 if (!this.paste_enabled) {
1021 1021 $('#paste_cell_replace').removeClass('disabled')
1022 1022 .on('click', function () {that.paste_cell_replace();});
1023 1023 $('#paste_cell_above').removeClass('disabled')
1024 1024 .on('click', function () {that.paste_cell_above();});
1025 1025 $('#paste_cell_below').removeClass('disabled')
1026 1026 .on('click', function () {that.paste_cell_below();});
1027 1027 this.paste_enabled = true;
1028 1028 };
1029 1029 };
1030 1030
1031 1031 /**
1032 1032 * Disable UI elements for pasting cells.
1033 1033 *
1034 1034 * @method disable_paste
1035 1035 */
1036 1036 Notebook.prototype.disable_paste = function () {
1037 1037 if (this.paste_enabled) {
1038 1038 $('#paste_cell_replace').addClass('disabled').off('click');
1039 1039 $('#paste_cell_above').addClass('disabled').off('click');
1040 1040 $('#paste_cell_below').addClass('disabled').off('click');
1041 1041 this.paste_enabled = false;
1042 1042 };
1043 1043 };
1044 1044
1045 1045 /**
1046 1046 * Cut a cell.
1047 1047 *
1048 1048 * @method cut_cell
1049 1049 */
1050 1050 Notebook.prototype.cut_cell = function () {
1051 1051 this.copy_cell();
1052 1052 this.delete_cell();
1053 1053 }
1054 1054
1055 1055 /**
1056 1056 * Copy a cell.
1057 1057 *
1058 1058 * @method copy_cell
1059 1059 */
1060 1060 Notebook.prototype.copy_cell = function () {
1061 1061 var cell = this.get_selected_cell();
1062 1062 this.clipboard = cell.toJSON();
1063 1063 this.enable_paste();
1064 1064 };
1065 1065
1066 1066 /**
1067 1067 * Replace the selected cell with a cell in the clipboard.
1068 1068 *
1069 1069 * @method paste_cell_replace
1070 1070 */
1071 1071 Notebook.prototype.paste_cell_replace = function () {
1072 1072 if (this.clipboard !== null && this.paste_enabled) {
1073 1073 var cell_data = this.clipboard;
1074 1074 var new_cell = this.insert_cell_above(cell_data.cell_type);
1075 1075 new_cell.fromJSON(cell_data);
1076 1076 var old_cell = this.get_next_cell(new_cell);
1077 1077 this.delete_cell(this.find_cell_index(old_cell));
1078 1078 this.select(this.find_cell_index(new_cell));
1079 1079 };
1080 1080 };
1081 1081
1082 1082 /**
1083 1083 * Paste a cell from the clipboard above the selected cell.
1084 1084 *
1085 1085 * @method paste_cell_above
1086 1086 */
1087 1087 Notebook.prototype.paste_cell_above = function () {
1088 1088 if (this.clipboard !== null && this.paste_enabled) {
1089 1089 var cell_data = this.clipboard;
1090 1090 var new_cell = this.insert_cell_above(cell_data.cell_type);
1091 1091 new_cell.fromJSON(cell_data);
1092 1092 };
1093 1093 };
1094 1094
1095 1095 /**
1096 1096 * Paste a cell from the clipboard below the selected cell.
1097 1097 *
1098 1098 * @method paste_cell_below
1099 1099 */
1100 1100 Notebook.prototype.paste_cell_below = function () {
1101 1101 if (this.clipboard !== null && this.paste_enabled) {
1102 1102 var cell_data = this.clipboard;
1103 1103 var new_cell = this.insert_cell_below(cell_data.cell_type);
1104 1104 new_cell.fromJSON(cell_data);
1105 1105 };
1106 1106 };
1107 1107
1108 1108 // Cell undelete
1109 1109
1110 1110 /**
1111 1111 * Restore the most recently deleted cell.
1112 1112 *
1113 1113 * @method undelete
1114 1114 */
1115 1115 Notebook.prototype.undelete = function() {
1116 1116 if (this.undelete_backup !== null && this.undelete_index !== null) {
1117 1117 var current_index = this.get_selected_index();
1118 1118 if (this.undelete_index < current_index) {
1119 1119 current_index = current_index + 1;
1120 1120 }
1121 1121 if (this.undelete_index >= this.ncells()) {
1122 1122 this.select(this.ncells() - 1);
1123 1123 }
1124 1124 else {
1125 1125 this.select(this.undelete_index);
1126 1126 }
1127 1127 var cell_data = this.undelete_backup;
1128 1128 var new_cell = null;
1129 1129 if (this.undelete_below) {
1130 1130 new_cell = this.insert_cell_below(cell_data.cell_type);
1131 1131 } else {
1132 1132 new_cell = this.insert_cell_above(cell_data.cell_type);
1133 1133 }
1134 1134 new_cell.fromJSON(cell_data);
1135 1135 this.select(current_index);
1136 1136 this.undelete_backup = null;
1137 1137 this.undelete_index = null;
1138 1138 }
1139 1139 $('#undelete_cell').addClass('disabled');
1140 1140 }
1141 1141
1142 1142 // Split/merge
1143 1143
1144 1144 /**
1145 1145 * Split the selected cell into two, at the cursor.
1146 1146 *
1147 1147 * @method split_cell
1148 1148 */
1149 1149 Notebook.prototype.split_cell = function () {
1150 1150 // Todo: implement spliting for other cell types.
1151 1151 var cell = this.get_selected_cell();
1152 1152 if (cell.is_splittable()) {
1153 1153 var texta = cell.get_pre_cursor();
1154 1154 var textb = cell.get_post_cursor();
1155 1155 if (cell instanceof IPython.CodeCell) {
1156 1156 cell.set_text(texta);
1157 1157 var new_cell = this.insert_cell_below('code');
1158 1158 new_cell.set_text(textb);
1159 1159 } else if (cell instanceof IPython.MarkdownCell) {
1160 1160 cell.set_text(texta);
1161 1161 cell.render();
1162 1162 var new_cell = this.insert_cell_below('markdown');
1163 1163 new_cell.edit(); // editor must be visible to call set_text
1164 1164 new_cell.set_text(textb);
1165 1165 new_cell.render();
1166 1166 }
1167 1167 };
1168 1168 };
1169 1169
1170 1170 /**
1171 1171 * Combine the selected cell into the cell above it.
1172 1172 *
1173 1173 * @method merge_cell_above
1174 1174 */
1175 1175 Notebook.prototype.merge_cell_above = function () {
1176 1176 var index = this.get_selected_index();
1177 1177 var cell = this.get_cell(index);
1178 1178 if (!cell.is_mergeable()) {
1179 1179 return;
1180 1180 }
1181 1181 if (index > 0) {
1182 1182 var upper_cell = this.get_cell(index-1);
1183 1183 if (!upper_cell.is_mergeable()) {
1184 1184 return;
1185 1185 }
1186 1186 var upper_text = upper_cell.get_text();
1187 1187 var text = cell.get_text();
1188 1188 if (cell instanceof IPython.CodeCell) {
1189 1189 cell.set_text(upper_text+'\n'+text);
1190 1190 } else if (cell instanceof IPython.MarkdownCell) {
1191 1191 cell.edit();
1192 1192 cell.set_text(upper_text+'\n'+text);
1193 1193 cell.render();
1194 1194 };
1195 1195 this.delete_cell(index-1);
1196 1196 this.select(this.find_cell_index(cell));
1197 1197 };
1198 1198 };
1199 1199
1200 1200 /**
1201 1201 * Combine the selected cell into the cell below it.
1202 1202 *
1203 1203 * @method merge_cell_below
1204 1204 */
1205 1205 Notebook.prototype.merge_cell_below = function () {
1206 1206 var index = this.get_selected_index();
1207 1207 var cell = this.get_cell(index);
1208 1208 if (!cell.is_mergeable()) {
1209 1209 return;
1210 1210 }
1211 1211 if (index < this.ncells()-1) {
1212 1212 var lower_cell = this.get_cell(index+1);
1213 1213 if (!lower_cell.is_mergeable()) {
1214 1214 return;
1215 1215 }
1216 1216 var lower_text = lower_cell.get_text();
1217 1217 var text = cell.get_text();
1218 1218 if (cell instanceof IPython.CodeCell) {
1219 1219 cell.set_text(text+'\n'+lower_text);
1220 1220 } else if (cell instanceof IPython.MarkdownCell) {
1221 1221 cell.edit();
1222 1222 cell.set_text(text+'\n'+lower_text);
1223 1223 cell.render();
1224 1224 };
1225 1225 this.delete_cell(index+1);
1226 1226 this.select(this.find_cell_index(cell));
1227 1227 };
1228 1228 };
1229 1229
1230 1230
1231 1231 // Cell collapsing and output clearing
1232 1232
1233 1233 /**
1234 1234 * Hide a cell's output.
1235 1235 *
1236 1236 * @method collapse
1237 1237 * @param {Number} index A cell's numeric index
1238 1238 */
1239 1239 Notebook.prototype.collapse = function (index) {
1240 1240 var i = this.index_or_selected(index);
1241 1241 this.get_cell(i).collapse();
1242 1242 this.set_dirty(true);
1243 1243 };
1244 1244
1245 1245 /**
1246 1246 * Show a cell's output.
1247 1247 *
1248 1248 * @method expand
1249 1249 * @param {Number} index A cell's numeric index
1250 1250 */
1251 1251 Notebook.prototype.expand = function (index) {
1252 1252 var i = this.index_or_selected(index);
1253 1253 this.get_cell(i).expand();
1254 1254 this.set_dirty(true);
1255 1255 };
1256 1256
1257 1257 /** Toggle whether a cell's output is collapsed or expanded.
1258 1258 *
1259 1259 * @method toggle_output
1260 1260 * @param {Number} index A cell's numeric index
1261 1261 */
1262 1262 Notebook.prototype.toggle_output = function (index) {
1263 1263 var i = this.index_or_selected(index);
1264 1264 this.get_cell(i).toggle_output();
1265 1265 this.set_dirty(true);
1266 1266 };
1267 1267
1268 1268 /**
1269 1269 * Toggle a scrollbar for long cell outputs.
1270 1270 *
1271 1271 * @method toggle_output_scroll
1272 1272 * @param {Number} index A cell's numeric index
1273 1273 */
1274 1274 Notebook.prototype.toggle_output_scroll = function (index) {
1275 1275 var i = this.index_or_selected(index);
1276 1276 this.get_cell(i).toggle_output_scroll();
1277 1277 };
1278 1278
1279 1279 /**
1280 1280 * Hide each code cell's output area.
1281 1281 *
1282 1282 * @method collapse_all_output
1283 1283 */
1284 1284 Notebook.prototype.collapse_all_output = function () {
1285 1285 var ncells = this.ncells();
1286 1286 var cells = this.get_cells();
1287 1287 for (var i=0; i<ncells; i++) {
1288 1288 if (cells[i] instanceof IPython.CodeCell) {
1289 1289 cells[i].output_area.collapse();
1290 1290 }
1291 1291 };
1292 1292 // this should not be set if the `collapse` key is removed from nbformat
1293 1293 this.set_dirty(true);
1294 1294 };
1295 1295
1296 1296 /**
1297 1297 * Expand each code cell's output area, and add a scrollbar for long output.
1298 1298 *
1299 1299 * @method scroll_all_output
1300 1300 */
1301 1301 Notebook.prototype.scroll_all_output = function () {
1302 1302 var ncells = this.ncells();
1303 1303 var cells = this.get_cells();
1304 1304 for (var i=0; i<ncells; i++) {
1305 1305 if (cells[i] instanceof IPython.CodeCell) {
1306 1306 cells[i].output_area.expand();
1307 1307 cells[i].output_area.scroll_if_long();
1308 1308 }
1309 1309 };
1310 1310 // this should not be set if the `collapse` key is removed from nbformat
1311 1311 this.set_dirty(true);
1312 1312 };
1313 1313
1314 1314 /**
1315 1315 * Expand each code cell's output area, and remove scrollbars.
1316 1316 *
1317 1317 * @method expand_all_output
1318 1318 */
1319 1319 Notebook.prototype.expand_all_output = function () {
1320 1320 var ncells = this.ncells();
1321 1321 var cells = this.get_cells();
1322 1322 for (var i=0; i<ncells; i++) {
1323 1323 if (cells[i] instanceof IPython.CodeCell) {
1324 1324 cells[i].output_area.expand();
1325 1325 cells[i].output_area.unscroll_area();
1326 1326 }
1327 1327 };
1328 1328 // this should not be set if the `collapse` key is removed from nbformat
1329 1329 this.set_dirty(true);
1330 1330 };
1331 1331
1332 1332 /**
1333 1333 * Clear each code cell's output area.
1334 1334 *
1335 1335 * @method clear_all_output
1336 1336 */
1337 1337 Notebook.prototype.clear_all_output = function () {
1338 1338 var ncells = this.ncells();
1339 1339 var cells = this.get_cells();
1340 1340 for (var i=0; i<ncells; i++) {
1341 1341 if (cells[i] instanceof IPython.CodeCell) {
1342 cells[i].clear_output(true,true,true);
1342 cells[i].clear_output();
1343 1343 // Make all In[] prompts blank, as well
1344 1344 // TODO: make this configurable (via checkbox?)
1345 1345 cells[i].set_input_prompt();
1346 1346 }
1347 1347 };
1348 1348 this.set_dirty(true);
1349 1349 };
1350 1350
1351 1351
1352 1352 // Other cell functions: line numbers, ...
1353 1353
1354 1354 /**
1355 1355 * Toggle line numbers in the selected cell's input area.
1356 1356 *
1357 1357 * @method cell_toggle_line_numbers
1358 1358 */
1359 1359 Notebook.prototype.cell_toggle_line_numbers = function() {
1360 1360 this.get_selected_cell().toggle_line_numbers();
1361 1361 };
1362 1362
1363 1363 // Kernel related things
1364 1364
1365 1365 /**
1366 1366 * Start a new kernel and set it on each code cell.
1367 1367 *
1368 1368 * @method start_kernel
1369 1369 */
1370 1370 Notebook.prototype.start_kernel = function () {
1371 1371 var base_url = $('body').data('baseKernelUrl') + "kernels";
1372 1372 this.kernel = new IPython.Kernel(base_url);
1373 1373 this.kernel.start(this.notebook_id);
1374 1374 // Now that the kernel has been created, tell the CodeCells about it.
1375 1375 var ncells = this.ncells();
1376 1376 for (var i=0; i<ncells; i++) {
1377 1377 var cell = this.get_cell(i);
1378 1378 if (cell instanceof IPython.CodeCell) {
1379 1379 cell.set_kernel(this.kernel)
1380 1380 };
1381 1381 };
1382 1382 };
1383 1383
1384 1384 /**
1385 1385 * Prompt the user to restart the IPython kernel.
1386 1386 *
1387 1387 * @method restart_kernel
1388 1388 */
1389 1389 Notebook.prototype.restart_kernel = function () {
1390 1390 var that = this;
1391 1391 IPython.dialog.modal({
1392 1392 title : "Restart kernel or continue running?",
1393 1393 body : $("<p/>").html(
1394 1394 'Do you want to restart the current kernel? You will lose all variables defined in it.'
1395 1395 ),
1396 1396 buttons : {
1397 1397 "Continue running" : {},
1398 1398 "Restart" : {
1399 1399 "class" : "btn-danger",
1400 1400 "click" : function() {
1401 1401 that.kernel.restart();
1402 1402 }
1403 1403 }
1404 1404 }
1405 1405 });
1406 1406 };
1407 1407
1408 1408 /**
1409 1409 * Run the selected cell.
1410 1410 *
1411 1411 * Execute or render cell outputs.
1412 1412 *
1413 1413 * @method execute_selected_cell
1414 1414 * @param {Object} options Customize post-execution behavior
1415 1415 */
1416 1416 Notebook.prototype.execute_selected_cell = function (options) {
1417 1417 // add_new: should a new cell be added if we are at the end of the nb
1418 1418 // terminal: execute in terminal mode, which stays in the current cell
1419 1419 var default_options = {terminal: false, add_new: true};
1420 1420 $.extend(default_options, options);
1421 1421 var that = this;
1422 1422 var cell = that.get_selected_cell();
1423 1423 var cell_index = that.find_cell_index(cell);
1424 1424 if (cell instanceof IPython.CodeCell) {
1425 1425 cell.execute();
1426 1426 }
1427 1427 if (default_options.terminal) {
1428 1428 cell.select_all();
1429 1429 } else {
1430 1430 if ((cell_index === (that.ncells()-1)) && default_options.add_new) {
1431 1431 that.insert_cell_below('code');
1432 1432 // If we are adding a new cell at the end, scroll down to show it.
1433 1433 that.scroll_to_bottom();
1434 1434 } else {
1435 1435 that.select(cell_index+1);
1436 1436 };
1437 1437 };
1438 1438 this.set_dirty(true);
1439 1439 };
1440 1440
1441 1441 /**
1442 1442 * Execute all cells below the selected cell.
1443 1443 *
1444 1444 * @method execute_cells_below
1445 1445 */
1446 1446 Notebook.prototype.execute_cells_below = function () {
1447 1447 this.execute_cell_range(this.get_selected_index(), this.ncells());
1448 1448 this.scroll_to_bottom();
1449 1449 };
1450 1450
1451 1451 /**
1452 1452 * Execute all cells above the selected cell.
1453 1453 *
1454 1454 * @method execute_cells_above
1455 1455 */
1456 1456 Notebook.prototype.execute_cells_above = function () {
1457 1457 this.execute_cell_range(0, this.get_selected_index());
1458 1458 };
1459 1459
1460 1460 /**
1461 1461 * Execute all cells.
1462 1462 *
1463 1463 * @method execute_all_cells
1464 1464 */
1465 1465 Notebook.prototype.execute_all_cells = function () {
1466 1466 this.execute_cell_range(0, this.ncells());
1467 1467 this.scroll_to_bottom();
1468 1468 };
1469 1469
1470 1470 /**
1471 1471 * Execute a contiguous range of cells.
1472 1472 *
1473 1473 * @method execute_cell_range
1474 1474 * @param {Number} start Index of the first cell to execute (inclusive)
1475 1475 * @param {Number} end Index of the last cell to execute (exclusive)
1476 1476 */
1477 1477 Notebook.prototype.execute_cell_range = function (start, end) {
1478 1478 for (var i=start; i<end; i++) {
1479 1479 this.select(i);
1480 1480 this.execute_selected_cell({add_new:false});
1481 1481 };
1482 1482 };
1483 1483
1484 1484 // Persistance and loading
1485 1485
1486 1486 /**
1487 1487 * Getter method for this notebook's ID.
1488 1488 *
1489 1489 * @method get_notebook_id
1490 1490 * @return {String} This notebook's ID
1491 1491 */
1492 1492 Notebook.prototype.get_notebook_id = function () {
1493 1493 return this.notebook_id;
1494 1494 };
1495 1495
1496 1496 /**
1497 1497 * Getter method for this notebook's name.
1498 1498 *
1499 1499 * @method get_notebook_name
1500 1500 * @return {String} This notebook's name
1501 1501 */
1502 1502 Notebook.prototype.get_notebook_name = function () {
1503 1503 return this.notebook_name;
1504 1504 };
1505 1505
1506 1506 /**
1507 1507 * Setter method for this notebook's name.
1508 1508 *
1509 1509 * @method set_notebook_name
1510 1510 * @param {String} name A new name for this notebook
1511 1511 */
1512 1512 Notebook.prototype.set_notebook_name = function (name) {
1513 1513 this.notebook_name = name;
1514 1514 };
1515 1515
1516 1516 /**
1517 1517 * Check that a notebook's name is valid.
1518 1518 *
1519 1519 * @method test_notebook_name
1520 1520 * @param {String} nbname A name for this notebook
1521 1521 * @return {Boolean} True if the name is valid, false if invalid
1522 1522 */
1523 1523 Notebook.prototype.test_notebook_name = function (nbname) {
1524 1524 nbname = nbname || '';
1525 1525 if (this.notebook_name_blacklist_re.test(nbname) == false && nbname.length>0) {
1526 1526 return true;
1527 1527 } else {
1528 1528 return false;
1529 1529 };
1530 1530 };
1531 1531
1532 1532 /**
1533 1533 * Load a notebook from JSON (.ipynb).
1534 1534 *
1535 1535 * This currently handles one worksheet: others are deleted.
1536 1536 *
1537 1537 * @method fromJSON
1538 1538 * @param {Object} data JSON representation of a notebook
1539 1539 */
1540 1540 Notebook.prototype.fromJSON = function (data) {
1541 1541 var ncells = this.ncells();
1542 1542 var i;
1543 1543 for (i=0; i<ncells; i++) {
1544 1544 // Always delete cell 0 as they get renumbered as they are deleted.
1545 1545 this.delete_cell(0);
1546 1546 };
1547 1547 // Save the metadata and name.
1548 1548 this.metadata = data.metadata;
1549 1549 this.notebook_name = data.metadata.name;
1550 1550 // Only handle 1 worksheet for now.
1551 1551 var worksheet = data.worksheets[0];
1552 1552 if (worksheet !== undefined) {
1553 1553 if (worksheet.metadata) {
1554 1554 this.worksheet_metadata = worksheet.metadata;
1555 1555 }
1556 1556 var new_cells = worksheet.cells;
1557 1557 ncells = new_cells.length;
1558 1558 var cell_data = null;
1559 1559 var new_cell = null;
1560 1560 for (i=0; i<ncells; i++) {
1561 1561 cell_data = new_cells[i];
1562 1562 // VERSIONHACK: plaintext -> raw
1563 1563 // handle never-released plaintext name for raw cells
1564 1564 if (cell_data.cell_type === 'plaintext'){
1565 1565 cell_data.cell_type = 'raw';
1566 1566 }
1567 1567
1568 1568 new_cell = this.insert_cell_below(cell_data.cell_type);
1569 1569 new_cell.fromJSON(cell_data);
1570 1570 };
1571 1571 };
1572 1572 if (data.worksheets.length > 1) {
1573 1573 IPython.dialog.modal({
1574 1574 title : "Multiple worksheets",
1575 1575 body : "This notebook has " + data.worksheets.length + " worksheets, " +
1576 1576 "but this version of IPython can only handle the first. " +
1577 1577 "If you save this notebook, worksheets after the first will be lost.",
1578 1578 buttons : {
1579 1579 OK : {
1580 1580 class : "btn-danger"
1581 1581 }
1582 1582 }
1583 1583 });
1584 1584 }
1585 1585 };
1586 1586
1587 1587 /**
1588 1588 * Dump this notebook into a JSON-friendly object.
1589 1589 *
1590 1590 * @method toJSON
1591 1591 * @return {Object} A JSON-friendly representation of this notebook.
1592 1592 */
1593 1593 Notebook.prototype.toJSON = function () {
1594 1594 var cells = this.get_cells();
1595 1595 var ncells = cells.length;
1596 1596 var cell_array = new Array(ncells);
1597 1597 for (var i=0; i<ncells; i++) {
1598 1598 cell_array[i] = cells[i].toJSON();
1599 1599 };
1600 1600 var data = {
1601 1601 // Only handle 1 worksheet for now.
1602 1602 worksheets : [{
1603 1603 cells: cell_array,
1604 1604 metadata: this.worksheet_metadata
1605 1605 }],
1606 1606 metadata : this.metadata
1607 1607 };
1608 1608 return data;
1609 1609 };
1610 1610
1611 1611 /**
1612 1612 * Start an autosave timer, for periodically saving the notebook.
1613 1613 *
1614 1614 * @method set_autosave_interval
1615 1615 * @param {Integer} interval the autosave interval in milliseconds
1616 1616 */
1617 1617 Notebook.prototype.set_autosave_interval = function (interval) {
1618 1618 var that = this;
1619 1619 // clear previous interval, so we don't get simultaneous timers
1620 1620 if (this.autosave_timer) {
1621 1621 clearInterval(this.autosave_timer);
1622 1622 }
1623 1623
1624 1624 this.autosave_interval = this.minimum_autosave_interval = interval;
1625 1625 if (interval) {
1626 1626 this.autosave_timer = setInterval(function() {
1627 1627 if (that.dirty) {
1628 1628 that.save_notebook();
1629 1629 }
1630 1630 }, interval);
1631 1631 $([IPython.events]).trigger("autosave_enabled.Notebook", interval);
1632 1632 } else {
1633 1633 this.autosave_timer = null;
1634 1634 $([IPython.events]).trigger("autosave_disabled.Notebook");
1635 1635 };
1636 1636 };
1637 1637
1638 1638 /**
1639 1639 * Save this notebook on the server.
1640 1640 *
1641 1641 * @method save_notebook
1642 1642 */
1643 1643 Notebook.prototype.save_notebook = function () {
1644 1644 // We may want to move the name/id/nbformat logic inside toJSON?
1645 1645 var data = this.toJSON();
1646 1646 data.metadata.name = this.notebook_name;
1647 1647 data.nbformat = this.nbformat;
1648 1648 data.nbformat_minor = this.nbformat_minor;
1649 1649
1650 1650 // time the ajax call for autosave tuning purposes.
1651 1651 var start = new Date().getTime();
1652 1652
1653 1653 // We do the call with settings so we can set cache to false.
1654 1654 var settings = {
1655 1655 processData : false,
1656 1656 cache : false,
1657 1657 type : "PUT",
1658 1658 data : JSON.stringify(data),
1659 1659 headers : {'Content-Type': 'application/json'},
1660 1660 success : $.proxy(this.save_notebook_success, this, start),
1661 1661 error : $.proxy(this.save_notebook_error, this)
1662 1662 };
1663 1663 $([IPython.events]).trigger('notebook_saving.Notebook');
1664 1664 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1665 1665 $.ajax(url, settings);
1666 1666 };
1667 1667
1668 1668 /**
1669 1669 * Success callback for saving a notebook.
1670 1670 *
1671 1671 * @method save_notebook_success
1672 1672 * @param {Integer} start the time when the save request started
1673 1673 * @param {Object} data JSON representation of a notebook
1674 1674 * @param {String} status Description of response status
1675 1675 * @param {jqXHR} xhr jQuery Ajax object
1676 1676 */
1677 1677 Notebook.prototype.save_notebook_success = function (start, data, status, xhr) {
1678 1678 this.set_dirty(false);
1679 1679 $([IPython.events]).trigger('notebook_saved.Notebook');
1680 1680 this._update_autosave_interval(start);
1681 1681 if (this._checkpoint_after_save) {
1682 1682 this.create_checkpoint();
1683 1683 this._checkpoint_after_save = false;
1684 1684 };
1685 1685 };
1686 1686
1687 1687 /**
1688 1688 * update the autosave interval based on how long the last save took
1689 1689 *
1690 1690 * @method _update_autosave_interval
1691 1691 * @param {Integer} timestamp when the save request started
1692 1692 */
1693 1693 Notebook.prototype._update_autosave_interval = function (start) {
1694 1694 var duration = (new Date().getTime() - start);
1695 1695 if (this.autosave_interval) {
1696 1696 // new save interval: higher of 10x save duration or parameter (default 30 seconds)
1697 1697 var interval = Math.max(10 * duration, this.minimum_autosave_interval);
1698 1698 // round to 10 seconds, otherwise we will be setting a new interval too often
1699 1699 interval = 10000 * Math.round(interval / 10000);
1700 1700 // set new interval, if it's changed
1701 1701 if (interval != this.autosave_interval) {
1702 1702 this.set_autosave_interval(interval);
1703 1703 }
1704 1704 }
1705 1705 };
1706 1706
1707 1707 /**
1708 1708 * Failure callback for saving a notebook.
1709 1709 *
1710 1710 * @method save_notebook_error
1711 1711 * @param {jqXHR} xhr jQuery Ajax object
1712 1712 * @param {String} status Description of response status
1713 1713 * @param {String} error_msg HTTP error message
1714 1714 */
1715 1715 Notebook.prototype.save_notebook_error = function (xhr, status, error_msg) {
1716 1716 $([IPython.events]).trigger('notebook_save_failed.Notebook');
1717 1717 };
1718 1718
1719 1719 /**
1720 1720 * Request a notebook's data from the server.
1721 1721 *
1722 1722 * @method load_notebook
1723 1723 * @param {String} notebook_id A notebook to load
1724 1724 */
1725 1725 Notebook.prototype.load_notebook = function (notebook_id) {
1726 1726 var that = this;
1727 1727 this.notebook_id = notebook_id;
1728 1728 // We do the call with settings so we can set cache to false.
1729 1729 var settings = {
1730 1730 processData : false,
1731 1731 cache : false,
1732 1732 type : "GET",
1733 1733 dataType : "json",
1734 1734 success : $.proxy(this.load_notebook_success,this),
1735 1735 error : $.proxy(this.load_notebook_error,this),
1736 1736 };
1737 1737 $([IPython.events]).trigger('notebook_loading.Notebook');
1738 1738 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id;
1739 1739 $.ajax(url, settings);
1740 1740 };
1741 1741
1742 1742 /**
1743 1743 * Success callback for loading a notebook from the server.
1744 1744 *
1745 1745 * Load notebook data from the JSON response.
1746 1746 *
1747 1747 * @method load_notebook_success
1748 1748 * @param {Object} data JSON representation of a notebook
1749 1749 * @param {String} status Description of response status
1750 1750 * @param {jqXHR} xhr jQuery Ajax object
1751 1751 */
1752 1752 Notebook.prototype.load_notebook_success = function (data, status, xhr) {
1753 1753 this.fromJSON(data);
1754 1754 if (this.ncells() === 0) {
1755 1755 this.insert_cell_below('code');
1756 1756 };
1757 1757 this.set_dirty(false);
1758 1758 this.select(0);
1759 1759 this.scroll_to_top();
1760 1760 if (data.orig_nbformat !== undefined && data.nbformat !== data.orig_nbformat) {
1761 1761 var msg = "This notebook has been converted from an older " +
1762 1762 "notebook format (v"+data.orig_nbformat+") to the current notebook " +
1763 1763 "format (v"+data.nbformat+"). The next time you save this notebook, the " +
1764 1764 "newer notebook format will be used and older versions of IPython " +
1765 1765 "may not be able to read it. To keep the older version, close the " +
1766 1766 "notebook without saving it.";
1767 1767 IPython.dialog.modal({
1768 1768 title : "Notebook converted",
1769 1769 body : msg,
1770 1770 buttons : {
1771 1771 OK : {
1772 1772 class : "btn-primary"
1773 1773 }
1774 1774 }
1775 1775 });
1776 1776 } else if (data.orig_nbformat_minor !== undefined && data.nbformat_minor !== data.orig_nbformat_minor) {
1777 1777 var that = this;
1778 1778 var orig_vs = 'v' + data.nbformat + '.' + data.orig_nbformat_minor;
1779 1779 var this_vs = 'v' + data.nbformat + '.' + this.nbformat_minor;
1780 1780 var msg = "This notebook is version " + orig_vs + ", but we only fully support up to " +
1781 1781 this_vs + ". You can still work with this notebook, but some features " +
1782 1782 "introduced in later notebook versions may not be available."
1783 1783
1784 1784 IPython.dialog.modal({
1785 1785 title : "Newer Notebook",
1786 1786 body : msg,
1787 1787 buttons : {
1788 1788 OK : {
1789 1789 class : "btn-danger"
1790 1790 }
1791 1791 }
1792 1792 });
1793 1793
1794 1794 }
1795 1795
1796 1796 // Create the kernel after the notebook is completely loaded to prevent
1797 1797 // code execution upon loading, which is a security risk.
1798 1798 this.start_kernel();
1799 1799 // load our checkpoint list
1800 1800 IPython.notebook.list_checkpoints();
1801 1801
1802 1802 $([IPython.events]).trigger('notebook_loaded.Notebook');
1803 1803 };
1804 1804
1805 1805 /**
1806 1806 * Failure callback for loading a notebook from the server.
1807 1807 *
1808 1808 * @method load_notebook_error
1809 1809 * @param {jqXHR} xhr jQuery Ajax object
1810 1810 * @param {String} textStatus Description of response status
1811 1811 * @param {String} errorThrow HTTP error message
1812 1812 */
1813 1813 Notebook.prototype.load_notebook_error = function (xhr, textStatus, errorThrow) {
1814 1814 if (xhr.status === 400) {
1815 1815 var msg = errorThrow;
1816 1816 } else if (xhr.status === 500) {
1817 1817 var msg = "An unknown error occurred while loading this notebook. " +
1818 1818 "This version can load notebook formats " +
1819 1819 "v" + this.nbformat + " or earlier.";
1820 1820 }
1821 1821 IPython.dialog.modal({
1822 1822 title: "Error loading notebook",
1823 1823 body : msg,
1824 1824 buttons : {
1825 1825 "OK": {}
1826 1826 }
1827 1827 });
1828 1828 }
1829 1829
1830 1830 /********************* checkpoint-related *********************/
1831 1831
1832 1832 /**
1833 1833 * Save the notebook then immediately create a checkpoint.
1834 1834 *
1835 1835 * @method save_checkpoint
1836 1836 */
1837 1837 Notebook.prototype.save_checkpoint = function () {
1838 1838 this._checkpoint_after_save = true;
1839 1839 this.save_notebook();
1840 1840 };
1841 1841
1842 1842 /**
1843 1843 * Add a checkpoint for this notebook.
1844 1844 * for use as a callback from checkpoint creation.
1845 1845 *
1846 1846 * @method add_checkpoint
1847 1847 */
1848 1848 Notebook.prototype.add_checkpoint = function (checkpoint) {
1849 1849 var found = false;
1850 1850 for (var i = 0; i < this.checkpoints.length; i++) {
1851 1851 var existing = this.checkpoints[i];
1852 1852 if (existing.checkpoint_id == checkpoint.checkpoint_id) {
1853 1853 found = true;
1854 1854 this.checkpoints[i] = checkpoint;
1855 1855 break;
1856 1856 }
1857 1857 }
1858 1858 if (!found) {
1859 1859 this.checkpoints.push(checkpoint);
1860 1860 }
1861 1861 this.last_checkpoint = this.checkpoints[this.checkpoints.length - 1];
1862 1862 };
1863 1863
1864 1864 /**
1865 1865 * List checkpoints for this notebook.
1866 1866 *
1867 1867 * @method list_checkpoints
1868 1868 */
1869 1869 Notebook.prototype.list_checkpoints = function () {
1870 1870 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1871 1871 $.get(url).done(
1872 1872 $.proxy(this.list_checkpoints_success, this)
1873 1873 ).fail(
1874 1874 $.proxy(this.list_checkpoints_error, this)
1875 1875 );
1876 1876 };
1877 1877
1878 1878 /**
1879 1879 * Success callback for listing checkpoints.
1880 1880 *
1881 1881 * @method list_checkpoint_success
1882 1882 * @param {Object} data JSON representation of a checkpoint
1883 1883 * @param {String} status Description of response status
1884 1884 * @param {jqXHR} xhr jQuery Ajax object
1885 1885 */
1886 1886 Notebook.prototype.list_checkpoints_success = function (data, status, xhr) {
1887 1887 var data = $.parseJSON(data);
1888 1888 this.checkpoints = data;
1889 1889 if (data.length) {
1890 1890 this.last_checkpoint = data[data.length - 1];
1891 1891 } else {
1892 1892 this.last_checkpoint = null;
1893 1893 }
1894 1894 $([IPython.events]).trigger('checkpoints_listed.Notebook', [data]);
1895 1895 };
1896 1896
1897 1897 /**
1898 1898 * Failure callback for listing a checkpoint.
1899 1899 *
1900 1900 * @method list_checkpoint_error
1901 1901 * @param {jqXHR} xhr jQuery Ajax object
1902 1902 * @param {String} status Description of response status
1903 1903 * @param {String} error_msg HTTP error message
1904 1904 */
1905 1905 Notebook.prototype.list_checkpoints_error = function (xhr, status, error_msg) {
1906 1906 $([IPython.events]).trigger('list_checkpoints_failed.Notebook');
1907 1907 };
1908 1908
1909 1909 /**
1910 1910 * Create a checkpoint of this notebook on the server from the most recent save.
1911 1911 *
1912 1912 * @method create_checkpoint
1913 1913 */
1914 1914 Notebook.prototype.create_checkpoint = function () {
1915 1915 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints';
1916 1916 $.post(url).done(
1917 1917 $.proxy(this.create_checkpoint_success, this)
1918 1918 ).fail(
1919 1919 $.proxy(this.create_checkpoint_error, this)
1920 1920 );
1921 1921 };
1922 1922
1923 1923 /**
1924 1924 * Success callback for creating a checkpoint.
1925 1925 *
1926 1926 * @method create_checkpoint_success
1927 1927 * @param {Object} data JSON representation of a checkpoint
1928 1928 * @param {String} status Description of response status
1929 1929 * @param {jqXHR} xhr jQuery Ajax object
1930 1930 */
1931 1931 Notebook.prototype.create_checkpoint_success = function (data, status, xhr) {
1932 1932 var data = $.parseJSON(data);
1933 1933 this.add_checkpoint(data);
1934 1934 $([IPython.events]).trigger('checkpoint_created.Notebook', data);
1935 1935 };
1936 1936
1937 1937 /**
1938 1938 * Failure callback for creating a checkpoint.
1939 1939 *
1940 1940 * @method create_checkpoint_error
1941 1941 * @param {jqXHR} xhr jQuery Ajax object
1942 1942 * @param {String} status Description of response status
1943 1943 * @param {String} error_msg HTTP error message
1944 1944 */
1945 1945 Notebook.prototype.create_checkpoint_error = function (xhr, status, error_msg) {
1946 1946 $([IPython.events]).trigger('checkpoint_failed.Notebook');
1947 1947 };
1948 1948
1949 1949 Notebook.prototype.restore_checkpoint_dialog = function (checkpoint) {
1950 1950 var that = this;
1951 1951 var checkpoint = checkpoint || this.last_checkpoint;
1952 1952 if ( ! checkpoint ) {
1953 1953 console.log("restore dialog, but no checkpoint to restore to!");
1954 1954 return;
1955 1955 }
1956 1956 var body = $('<div/>').append(
1957 1957 $('<p/>').addClass("p-space").text(
1958 1958 "Are you sure you want to revert the notebook to " +
1959 1959 "the latest checkpoint?"
1960 1960 ).append(
1961 1961 $("<strong/>").text(
1962 1962 " This cannot be undone."
1963 1963 )
1964 1964 )
1965 1965 ).append(
1966 1966 $('<p/>').addClass("p-space").text("The checkpoint was last updated at:")
1967 1967 ).append(
1968 1968 $('<p/>').addClass("p-space").text(
1969 1969 Date(checkpoint.last_modified)
1970 1970 ).css("text-align", "center")
1971 1971 );
1972 1972
1973 1973 IPython.dialog.modal({
1974 1974 title : "Revert notebook to checkpoint",
1975 1975 body : body,
1976 1976 buttons : {
1977 1977 Revert : {
1978 1978 class : "btn-danger",
1979 1979 click : function () {
1980 1980 that.restore_checkpoint(checkpoint.checkpoint_id);
1981 1981 }
1982 1982 },
1983 1983 Cancel : {}
1984 1984 }
1985 1985 });
1986 1986 }
1987 1987
1988 1988 /**
1989 1989 * Restore the notebook to a checkpoint state.
1990 1990 *
1991 1991 * @method restore_checkpoint
1992 1992 * @param {String} checkpoint ID
1993 1993 */
1994 1994 Notebook.prototype.restore_checkpoint = function (checkpoint) {
1995 1995 $([IPython.events]).trigger('checkpoint_restoring.Notebook', checkpoint);
1996 1996 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
1997 1997 $.post(url).done(
1998 1998 $.proxy(this.restore_checkpoint_success, this)
1999 1999 ).fail(
2000 2000 $.proxy(this.restore_checkpoint_error, this)
2001 2001 );
2002 2002 };
2003 2003
2004 2004 /**
2005 2005 * Success callback for restoring a notebook to a checkpoint.
2006 2006 *
2007 2007 * @method restore_checkpoint_success
2008 2008 * @param {Object} data (ignored, should be empty)
2009 2009 * @param {String} status Description of response status
2010 2010 * @param {jqXHR} xhr jQuery Ajax object
2011 2011 */
2012 2012 Notebook.prototype.restore_checkpoint_success = function (data, status, xhr) {
2013 2013 $([IPython.events]).trigger('checkpoint_restored.Notebook');
2014 2014 this.load_notebook(this.notebook_id);
2015 2015 };
2016 2016
2017 2017 /**
2018 2018 * Failure callback for restoring a notebook to a checkpoint.
2019 2019 *
2020 2020 * @method restore_checkpoint_error
2021 2021 * @param {jqXHR} xhr jQuery Ajax object
2022 2022 * @param {String} status Description of response status
2023 2023 * @param {String} error_msg HTTP error message
2024 2024 */
2025 2025 Notebook.prototype.restore_checkpoint_error = function (xhr, status, error_msg) {
2026 2026 $([IPython.events]).trigger('checkpoint_restore_failed.Notebook');
2027 2027 };
2028 2028
2029 2029 /**
2030 2030 * Delete a notebook checkpoint.
2031 2031 *
2032 2032 * @method delete_checkpoint
2033 2033 * @param {String} checkpoint ID
2034 2034 */
2035 2035 Notebook.prototype.delete_checkpoint = function (checkpoint) {
2036 2036 $([IPython.events]).trigger('checkpoint_deleting.Notebook', checkpoint);
2037 2037 var url = this.baseProjectUrl() + 'notebooks/' + this.notebook_id + '/checkpoints/' + checkpoint;
2038 2038 $.ajax(url, {
2039 2039 type: 'DELETE',
2040 2040 success: $.proxy(this.delete_checkpoint_success, this),
2041 2041 error: $.proxy(this.delete_notebook_error,this)
2042 2042 });
2043 2043 };
2044 2044
2045 2045 /**
2046 2046 * Success callback for deleting a notebook checkpoint
2047 2047 *
2048 2048 * @method delete_checkpoint_success
2049 2049 * @param {Object} data (ignored, should be empty)
2050 2050 * @param {String} status Description of response status
2051 2051 * @param {jqXHR} xhr jQuery Ajax object
2052 2052 */
2053 2053 Notebook.prototype.delete_checkpoint_success = function (data, status, xhr) {
2054 2054 $([IPython.events]).trigger('checkpoint_deleted.Notebook', data);
2055 2055 this.load_notebook(this.notebook_id);
2056 2056 };
2057 2057
2058 2058 /**
2059 2059 * Failure callback for deleting a notebook checkpoint.
2060 2060 *
2061 2061 * @method delete_checkpoint_error
2062 2062 * @param {jqXHR} xhr jQuery Ajax object
2063 2063 * @param {String} status Description of response status
2064 2064 * @param {String} error_msg HTTP error message
2065 2065 */
2066 2066 Notebook.prototype.delete_checkpoint_error = function (xhr, status, error_msg) {
2067 2067 $([IPython.events]).trigger('checkpoint_delete_failed.Notebook');
2068 2068 };
2069 2069
2070 2070
2071 2071 IPython.Notebook = Notebook;
2072 2072
2073 2073
2074 2074 return IPython;
2075 2075
2076 2076 }(IPython));
2077 2077
@@ -1,682 +1,651 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // OutputArea
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 * @submodule OutputArea
16 16 */
17 17 var IPython = (function (IPython) {
18 18 "use strict";
19 19
20 20 var utils = IPython.utils;
21 21
22 22 /**
23 23 * @class OutputArea
24 24 *
25 25 * @constructor
26 26 */
27 27
28 28 var OutputArea = function (selector, prompt_area) {
29 29 this.selector = selector;
30 30 this.wrapper = $(selector);
31 31 this.outputs = [];
32 32 this.collapsed = false;
33 33 this.scrolled = false;
34 34 this.clear_out_timeout = null;
35 35 if (prompt_area === undefined) {
36 36 this.prompt_area = true;
37 37 } else {
38 38 this.prompt_area = prompt_area;
39 39 }
40 40 this.create_elements();
41 41 this.style();
42 42 this.bind_events();
43 43 };
44 44
45 45 OutputArea.prototype.create_elements = function () {
46 46 this.element = $("<div/>");
47 47 this.collapse_button = $("<div/>");
48 48 this.prompt_overlay = $("<div/>");
49 49 this.wrapper.append(this.prompt_overlay);
50 50 this.wrapper.append(this.element);
51 51 this.wrapper.append(this.collapse_button);
52 52 };
53 53
54 54
55 55 OutputArea.prototype.style = function () {
56 56 this.collapse_button.hide();
57 57 this.prompt_overlay.hide();
58 58
59 59 this.wrapper.addClass('output_wrapper');
60 60 this.element.addClass('output vbox');
61 61
62 62 this.collapse_button.addClass("btn output_collapsed");
63 63 this.collapse_button.attr('title', 'click to expand output');
64 64 this.collapse_button.html('. . .');
65 65
66 66 this.prompt_overlay.addClass('out_prompt_overlay prompt');
67 67 this.prompt_overlay.attr('title', 'click to expand output; double click to hide output');
68 68
69 69 this.collapse();
70 70 };
71 71
72 72 /**
73 73 * Should the OutputArea scroll?
74 74 * Returns whether the height (in lines) exceeds a threshold.
75 75 *
76 76 * @private
77 77 * @method _should_scroll
78 78 * @param [lines=100]{Integer}
79 79 * @return {Bool}
80 80 *
81 81 */
82 82 OutputArea.prototype._should_scroll = function (lines) {
83 83 if (lines <=0 ){ return }
84 84 if (!lines) {
85 85 lines = 100;
86 86 }
87 87 // line-height from http://stackoverflow.com/questions/1185151
88 88 var fontSize = this.element.css('font-size');
89 89 var lineHeight = Math.floor(parseInt(fontSize.replace('px','')) * 1.5);
90 90
91 91 return (this.element.height() > lines * lineHeight);
92 92 };
93 93
94 94
95 95 OutputArea.prototype.bind_events = function () {
96 96 var that = this;
97 97 this.prompt_overlay.dblclick(function () { that.toggle_output(); });
98 98 this.prompt_overlay.click(function () { that.toggle_scroll(); });
99 99
100 100 this.element.resize(function () {
101 101 // FIXME: Firefox on Linux misbehaves, so automatic scrolling is disabled
102 102 if ( IPython.utils.browser[0] === "Firefox" ) {
103 103 return;
104 104 }
105 105 // maybe scroll output,
106 106 // if it's grown large enough and hasn't already been scrolled.
107 107 if ( !that.scrolled && that._should_scroll(OutputArea.auto_scroll_threshold)) {
108 108 that.scroll_area();
109 109 }
110 110 });
111 111 this.collapse_button.click(function () {
112 112 that.expand();
113 113 });
114 114 };
115 115
116 116
117 117 OutputArea.prototype.collapse = function () {
118 118 if (!this.collapsed) {
119 119 this.element.hide();
120 120 this.prompt_overlay.hide();
121 121 if (this.element.html()){
122 122 this.collapse_button.show();
123 123 }
124 124 this.collapsed = true;
125 125 }
126 126 };
127 127
128 128
129 129 OutputArea.prototype.expand = function () {
130 130 if (this.collapsed) {
131 131 this.collapse_button.hide();
132 132 this.element.show();
133 133 this.prompt_overlay.show();
134 134 this.collapsed = false;
135 135 }
136 136 };
137 137
138 138
139 139 OutputArea.prototype.toggle_output = function () {
140 140 if (this.collapsed) {
141 141 this.expand();
142 142 } else {
143 143 this.collapse();
144 144 }
145 145 };
146 146
147 147
148 148 OutputArea.prototype.scroll_area = function () {
149 149 this.element.addClass('output_scroll');
150 150 this.prompt_overlay.attr('title', 'click to unscroll output; double click to hide');
151 151 this.scrolled = true;
152 152 };
153 153
154 154
155 155 OutputArea.prototype.unscroll_area = function () {
156 156 this.element.removeClass('output_scroll');
157 157 this.prompt_overlay.attr('title', 'click to scroll output; double click to hide');
158 158 this.scrolled = false;
159 159 };
160 160
161 161 /**
162 162 * Threshold to trigger autoscroll when the OutputArea is resized,
163 163 * typically when new outputs are added.
164 164 *
165 165 * Behavior is undefined if autoscroll is lower than minimum_scroll_threshold,
166 166 * unless it is < 0, in which case autoscroll will never be triggered
167 167 *
168 168 * @property auto_scroll_threshold
169 169 * @type Number
170 170 * @default 100
171 171 *
172 172 **/
173 173 OutputArea.auto_scroll_threshold = 100;
174 174
175 175
176 176 /**
177 177 * Lower limit (in lines) for OutputArea to be made scrollable. OutputAreas
178 178 * shorter than this are never scrolled.
179 179 *
180 180 * @property minimum_scroll_threshold
181 181 * @type Number
182 182 * @default 20
183 183 *
184 184 **/
185 185 OutputArea.minimum_scroll_threshold = 20;
186 186
187 187
188 188 /**
189 189 *
190 190 * Scroll OutputArea if height supperior than a threshold (in lines).
191 191 *
192 192 * Threshold is a maximum number of lines. If unspecified, defaults to
193 193 * OutputArea.minimum_scroll_threshold.
194 194 *
195 195 * Negative threshold will prevent the OutputArea from ever scrolling.
196 196 *
197 197 * @method scroll_if_long
198 198 *
199 199 * @param [lines=20]{Number} Default to 20 if not set,
200 200 * behavior undefined for value of `0`.
201 201 *
202 202 **/
203 203 OutputArea.prototype.scroll_if_long = function (lines) {
204 204 var n = lines | OutputArea.minimum_scroll_threshold;
205 205 if(n <= 0){
206 206 return
207 207 }
208 208
209 209 if (this._should_scroll(n)) {
210 210 // only allow scrolling long-enough output
211 211 this.scroll_area();
212 212 }
213 213 };
214 214
215 215
216 216 OutputArea.prototype.toggle_scroll = function () {
217 217 if (this.scrolled) {
218 218 this.unscroll_area();
219 219 } else {
220 220 // only allow scrolling long-enough output
221 221 this.scroll_if_long();
222 222 }
223 223 };
224 224
225 225
226 226 // typeset with MathJax if MathJax is available
227 227 OutputArea.prototype.typeset = function () {
228 228 if (window.MathJax){
229 229 MathJax.Hub.Queue(["Typeset",MathJax.Hub]);
230 230 }
231 231 };
232 232
233 233
234 234 OutputArea.prototype.handle_output = function (msg_type, content) {
235 235 var json = {};
236 236 json.output_type = msg_type;
237 237 if (msg_type === "stream") {
238 238 json.text = content.data;
239 239 json.stream = content.name;
240 240 } else if (msg_type === "display_data") {
241 241 json = this.convert_mime_types(json, content.data);
242 242 json.metadata = this.convert_mime_types({}, content.metadata);
243 243 } else if (msg_type === "pyout") {
244 244 json.prompt_number = content.execution_count;
245 245 json = this.convert_mime_types(json, content.data);
246 246 json.metadata = this.convert_mime_types({}, content.metadata);
247 247 } else if (msg_type === "pyerr") {
248 248 json.ename = content.ename;
249 249 json.evalue = content.evalue;
250 250 json.traceback = content.traceback;
251 251 }
252 252 // append with dynamic=true
253 253 this.append_output(json, true);
254 254 };
255 255
256 256
257 257 OutputArea.prototype.convert_mime_types = function (json, data) {
258 258 if (data === undefined) {
259 259 return json;
260 260 }
261 261 if (data['text/plain'] !== undefined) {
262 262 json.text = data['text/plain'];
263 263 }
264 264 if (data['text/html'] !== undefined) {
265 265 json.html = data['text/html'];
266 266 }
267 267 if (data['image/svg+xml'] !== undefined) {
268 268 json.svg = data['image/svg+xml'];
269 269 }
270 270 if (data['image/png'] !== undefined) {
271 271 json.png = data['image/png'];
272 272 }
273 273 if (data['image/jpeg'] !== undefined) {
274 274 json.jpeg = data['image/jpeg'];
275 275 }
276 276 if (data['text/latex'] !== undefined) {
277 277 json.latex = data['text/latex'];
278 278 }
279 279 if (data['application/json'] !== undefined) {
280 280 json.json = data['application/json'];
281 281 }
282 282 if (data['application/javascript'] !== undefined) {
283 283 json.javascript = data['application/javascript'];
284 284 }
285 285 return json;
286 286 };
287 287
288 288
289 289 OutputArea.prototype.append_output = function (json, dynamic) {
290 290 // If dynamic is true, javascript output will be eval'd.
291 291 this.expand();
292 292 if (json.output_type === 'pyout') {
293 293 this.append_pyout(json, dynamic);
294 294 } else if (json.output_type === 'pyerr') {
295 295 this.append_pyerr(json);
296 296 } else if (json.output_type === 'display_data') {
297 297 this.append_display_data(json, dynamic);
298 298 } else if (json.output_type === 'stream') {
299 299 this.append_stream(json);
300 300 }
301 301 this.outputs.push(json);
302 302 this.element.height('auto');
303 303 var that = this;
304 304 setTimeout(function(){that.element.trigger('resize');}, 100);
305 305 };
306 306
307 307
308 308 OutputArea.prototype.create_output_area = function () {
309 309 var oa = $("<div/>").addClass("output_area");
310 310 if (this.prompt_area) {
311 311 oa.append($('<div/>').addClass('prompt'));
312 312 }
313 313 return oa;
314 314 };
315 315
316 316 OutputArea.prototype._append_javascript_error = function (err, container) {
317 317 // display a message when a javascript error occurs in display output
318 318 var msg = "Javascript error adding output!"
319 319 console.log(msg, err);
320 320 if ( container === undefined ) return;
321 321 container.append(
322 322 $('<div/>').html(msg + "<br/>" +
323 323 err.toString() +
324 324 '<br/>See your browser Javascript console for more details.'
325 325 ).addClass('js-error')
326 326 );
327 327 container.show();
328 328 };
329 329
330 330 OutputArea.prototype._safe_append = function (toinsert) {
331 331 // safely append an item to the document
332 332 // this is an object created by user code,
333 333 // and may have errors, which should not be raised
334 334 // under any circumstances.
335 335 try {
336 336 this.element.append(toinsert);
337 337 } catch(err) {
338 338 console.log(err);
339 339 this._append_javascript_error(err, this.element);
340 340 }
341 341 };
342 342
343 343
344 344 OutputArea.prototype.append_pyout = function (json, dynamic) {
345 345 var n = json.prompt_number || ' ';
346 346 var toinsert = this.create_output_area();
347 347 if (this.prompt_area) {
348 348 toinsert.find('div.prompt').addClass('output_prompt').html('Out[' + n + ']:');
349 349 }
350 350 this.append_mime_type(json, toinsert, dynamic);
351 351 this._safe_append(toinsert);
352 352 // If we just output latex, typeset it.
353 353 if ((json.latex !== undefined) || (json.html !== undefined)) {
354 354 this.typeset();
355 355 }
356 356 };
357 357
358 358
359 359 OutputArea.prototype.append_pyerr = function (json) {
360 360 var tb = json.traceback;
361 361 if (tb !== undefined && tb.length > 0) {
362 362 var s = '';
363 363 var len = tb.length;
364 364 for (var i=0; i<len; i++) {
365 365 s = s + tb[i] + '\n';
366 366 }
367 367 s = s + '\n';
368 368 var toinsert = this.create_output_area();
369 369 this.append_text(s, {}, toinsert);
370 370 this._safe_append(toinsert);
371 371 }
372 372 };
373 373
374 374
375 375 OutputArea.prototype.append_stream = function (json) {
376 376 // temporary fix: if stream undefined (json file written prior to this patch),
377 377 // default to most likely stdout:
378 378 if (json.stream == undefined){
379 379 json.stream = 'stdout';
380 380 }
381 381 var text = json.text;
382 382 var subclass = "output_"+json.stream;
383 383 if (this.outputs.length > 0){
384 384 // have at least one output to consider
385 385 var last = this.outputs[this.outputs.length-1];
386 386 if (last.output_type == 'stream' && json.stream == last.stream){
387 387 // latest output was in the same stream,
388 388 // so append directly into its pre tag
389 389 // escape ANSI & HTML specials:
390 390 var pre = this.element.find('div.'+subclass).last().find('pre');
391 391 var html = utils.fixCarriageReturn(
392 392 pre.html() + utils.fixConsole(text));
393 393 pre.html(html);
394 394 return;
395 395 }
396 396 }
397 397
398 398 if (!text.replace("\r", "")) {
399 399 // text is nothing (empty string, \r, etc.)
400 400 // so don't append any elements, which might add undesirable space
401 401 return;
402 402 }
403 403
404 404 // If we got here, attach a new div
405 405 var toinsert = this.create_output_area();
406 406 this.append_text(text, {}, toinsert, "output_stream "+subclass);
407 407 this._safe_append(toinsert);
408 408 };
409 409
410 410
411 411 OutputArea.prototype.append_display_data = function (json, dynamic) {
412 412 var toinsert = this.create_output_area();
413 413 this.append_mime_type(json, toinsert, dynamic);
414 414 this._safe_append(toinsert);
415 415 // If we just output latex, typeset it.
416 416 if ( (json.latex !== undefined) || (json.html !== undefined) ) {
417 417 this.typeset();
418 418 }
419 419 };
420 420
421 421 OutputArea.display_order = ['javascript','html','latex','svg','png','jpeg','text'];
422 422
423 423 OutputArea.prototype.append_mime_type = function (json, element, dynamic) {
424 424 for(var type_i in OutputArea.display_order){
425 425 var type = OutputArea.display_order[type_i];
426 426 if(json[type] != undefined ){
427 427 var md = {};
428 428 if (json.metadata && json.metadata[type]) {
429 429 md = json.metadata[type];
430 430 };
431 431 if(type == 'javascript'){
432 432 if (dynamic) {
433 433 this.append_javascript(json.javascript, md, element, dynamic);
434 434 }
435 435 } else {
436 436 this['append_'+type](json[type], md, element);
437 437 }
438 438 return;
439 439 }
440 440 }
441 441 };
442 442
443 443
444 444 OutputArea.prototype.append_html = function (html, md, element) {
445 445 var toinsert = $("<div/>").addClass("output_subarea output_html rendered_html");
446 446 toinsert.append(html);
447 447 element.append(toinsert);
448 448 };
449 449
450 450
451 451 OutputArea.prototype.append_javascript = function (js, md, container) {
452 452 // We just eval the JS code, element appears in the local scope.
453 453 var element = $("<div/>").addClass("output_subarea");
454 454 container.append(element);
455 455 // Div for js shouldn't be drawn, as it will add empty height to the area.
456 456 container.hide();
457 457 // If the Javascript appends content to `element` that should be drawn, then
458 458 // it must also call `container.show()`.
459 459 try {
460 460 eval(js);
461 461 } catch(err) {
462 462 this._append_javascript_error(err, container);
463 463 }
464 464 };
465 465
466 466
467 467 OutputArea.prototype.append_text = function (data, md, element, extra_class) {
468 468 var toinsert = $("<div/>").addClass("output_subarea output_text");
469 469 // escape ANSI & HTML specials in plaintext:
470 470 data = utils.fixConsole(data);
471 471 data = utils.fixCarriageReturn(data);
472 472 data = utils.autoLinkUrls(data);
473 473 if (extra_class){
474 474 toinsert.addClass(extra_class);
475 475 }
476 476 toinsert.append($("<pre/>").html(data));
477 477 element.append(toinsert);
478 478 };
479 479
480 480
481 481 OutputArea.prototype.append_svg = function (svg, md, element) {
482 482 var toinsert = $("<div/>").addClass("output_subarea output_svg");
483 483 toinsert.append(svg);
484 484 element.append(toinsert);
485 485 };
486 486
487 487
488 488 OutputArea.prototype._dblclick_to_reset_size = function (img) {
489 489 // schedule wrapping image in resizable after a delay,
490 490 // so we don't end up calling resize on a zero-size object
491 491 var that = this;
492 492 setTimeout(function () {
493 493 var h0 = img.height();
494 494 var w0 = img.width();
495 495 if (!(h0 && w0)) {
496 496 // zero size, schedule another timeout
497 497 that._dblclick_to_reset_size(img);
498 498 return;
499 499 }
500 500 img.resizable({
501 501 aspectRatio: true,
502 502 autoHide: true
503 503 });
504 504 img.dblclick(function () {
505 505 // resize wrapper & image together for some reason:
506 506 img.parent().height(h0);
507 507 img.height(h0);
508 508 img.parent().width(w0);
509 509 img.width(w0);
510 510 });
511 511 }, 250);
512 512 };
513 513
514 514
515 515 OutputArea.prototype.append_png = function (png, md, element) {
516 516 var toinsert = $("<div/>").addClass("output_subarea output_png");
517 517 var img = $("<img/>").attr('src','data:image/png;base64,'+png);
518 518 if (md['height']) {
519 519 img.attr('height', md['height']);
520 520 }
521 521 if (md['width']) {
522 522 img.attr('width', md['width']);
523 523 }
524 524 this._dblclick_to_reset_size(img);
525 525 toinsert.append(img);
526 526 element.append(toinsert);
527 527 };
528 528
529 529
530 530 OutputArea.prototype.append_jpeg = function (jpeg, md, element) {
531 531 var toinsert = $("<div/>").addClass("output_subarea output_jpeg");
532 532 var img = $("<img/>").attr('src','data:image/jpeg;base64,'+jpeg);
533 533 if (md['height']) {
534 534 img.attr('height', md['height']);
535 535 }
536 536 if (md['width']) {
537 537 img.attr('width', md['width']);
538 538 }
539 539 this._dblclick_to_reset_size(img);
540 540 toinsert.append(img);
541 541 element.append(toinsert);
542 542 };
543 543
544 544
545 545 OutputArea.prototype.append_latex = function (latex, md, element) {
546 546 // This method cannot do the typesetting because the latex first has to
547 547 // be on the page.
548 548 var toinsert = $("<div/>").addClass("output_subarea output_latex");
549 549 toinsert.append(latex);
550 550 element.append(toinsert);
551 551 };
552 552
553 553 OutputArea.prototype.append_raw_input = function (content) {
554 554 var that = this;
555 555 this.expand();
556 556 var area = this.create_output_area();
557 557
558 558 // disable any other raw_inputs, if they are left around
559 559 $("div.output_subarea.raw_input").remove();
560 560
561 561 area.append(
562 562 $("<div/>")
563 563 .addClass("box-flex1 output_subarea raw_input")
564 564 .append(
565 565 $("<span/>")
566 566 .addClass("input_prompt")
567 567 .text(content.prompt)
568 568 )
569 569 .append(
570 570 $("<input/>")
571 571 .addClass("raw_input")
572 572 .attr('type', 'text')
573 573 .attr("size", 47)
574 574 .keydown(function (event, ui) {
575 575 // make sure we submit on enter,
576 576 // and don't re-execute the *cell* on shift-enter
577 577 if (event.which === utils.keycodes.ENTER) {
578 578 that._submit_raw_input();
579 579 return false;
580 580 }
581 581 })
582 582 )
583 583 );
584 584 this.element.append(area);
585 585 // weirdly need double-focus now,
586 586 // otherwise only the cell will be focused
587 587 area.find("input.raw_input").focus().focus();
588 588 }
589 589 OutputArea.prototype._submit_raw_input = function (evt) {
590 590 var container = this.element.find("div.raw_input");
591 591 var theprompt = container.find("span.input_prompt");
592 592 var theinput = container.find("input.raw_input");
593 593 var value = theinput.val();
594 594 var content = {
595 595 output_type : 'stream',
596 596 name : 'stdout',
597 597 text : theprompt.text() + value + '\n'
598 598 }
599 599 // remove form container
600 600 container.parent().remove();
601 601 // replace with plaintext version in stdout
602 602 this.append_output(content, false);
603 603 $([IPython.events]).trigger('send_input_reply.Kernel', value);
604 604 }
605 605
606 606
607 607 OutputArea.prototype.handle_clear_output = function (content) {
608 this.clear_output(content.stdout, content.stderr, content.other);
608 this.clear_output();
609 609 };
610 610
611 611
612 OutputArea.prototype.clear_output = function (stdout, stderr, other) {
613 var output_div = this.element;
614
612 OutputArea.prototype.clear_output = function() {
613
615 614 // Fix the output div's height
616 var height = output_div.height();
617 output_div.height(height);
615 var height = this.element.height();
616 this.element.height(height);
618 617
619 if (stdout && stderr && other){
620 // clear all, no need for logic
621 output_div.html("");
622 this.outputs = [];
623 this.unscroll_area();
624 return;
625 }
626 // remove html output
627 // each output_subarea that has an identifying class is in an output_area
628 // which is the element to be removed.
629 if (stdout) {
630 output_div.find("div.output_stdout").parent().remove();
631 }
632 if (stderr) {
633 output_div.find("div.output_stderr").parent().remove();
634 }
635 if (other) {
636 output_div.find("div.output_subarea").not("div.output_stderr").not("div.output_stdout").parent().remove();
637 }
618 // clear all, no need for logic
619 this.element.html("");
620 this.outputs = [];
638 621 this.unscroll_area();
639
640 // remove cleared outputs from JSON list:
641 for (var i = this.outputs.length - 1; i >= 0; i--) {
642 var out = this.outputs[i];
643 var output_type = out.output_type;
644 if (output_type == "display_data" && other) {
645 this.outputs.splice(i,1);
646 } else if (output_type == "stream") {
647 if (stdout && out.stream == "stdout") {
648 this.outputs.splice(i,1);
649 } else if (stderr && out.stream == "stderr") {
650 this.outputs.splice(i,1);
651 }
652 }
653 }
622 return;
654 623 };
655 624
656 625
657 626 // JSON serialization
658 627
659 628 OutputArea.prototype.fromJSON = function (outputs) {
660 629 var len = outputs.length;
661 630 for (var i=0; i<len; i++) {
662 631 // append with dynamic=false.
663 632 this.append_output(outputs[i], false);
664 633 }
665 634 };
666 635
667 636
668 637 OutputArea.prototype.toJSON = function () {
669 638 var outputs = [];
670 639 var len = this.outputs.length;
671 640 for (var i=0; i<len; i++) {
672 641 outputs[i] = this.outputs[i];
673 642 }
674 643 return outputs;
675 644 };
676 645
677 646
678 647 IPython.OutputArea = OutputArea;
679 648
680 649 return IPython;
681 650
682 651 }(IPython));
@@ -1,602 +1,599 b''
1 1 """A ZMQ-based subclass of InteractiveShell.
2 2
3 3 This code is meant to ease the refactoring of the base InteractiveShell into
4 4 something with a cleaner architecture for 2-process use, without actually
5 5 breaking InteractiveShell itself. So we're doing something a bit ugly, where
6 6 we subclass and override what we want to fix. Once this is working well, we
7 7 can go back to the base class and refactor the code for a cleaner inheritance
8 8 implementation that doesn't rely on so much monkeypatching.
9 9
10 10 But this lets us maintain a fully working IPython as we develop the new
11 11 machinery. This should thus be thought of as scaffolding.
12 12 """
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import print_function
17 17
18 18 # Stdlib
19 19 import os
20 20 import sys
21 21 import time
22 22
23 23 # System library imports
24 24 from zmq.eventloop import ioloop
25 25
26 26 # Our own
27 27 from IPython.core.interactiveshell import (
28 28 InteractiveShell, InteractiveShellABC
29 29 )
30 30 from IPython.core import page
31 31 from IPython.core.autocall import ZMQExitAutocall
32 32 from IPython.core.displaypub import DisplayPublisher
33 33 from IPython.core.error import UsageError
34 34 from IPython.core.magics import MacroToEdit, CodeMagics
35 35 from IPython.core.magic import magics_class, line_magic, Magics
36 36 from IPython.core.payloadpage import install_payload_page
37 37 from IPython.display import display, Javascript
38 38 from IPython.kernel.inprocess.socket import SocketABC
39 39 from IPython.kernel import (
40 40 get_connection_file, get_connection_info, connect_qtconsole
41 41 )
42 42 from IPython.testing.skipdoctest import skip_doctest
43 43 from IPython.utils import openpy
44 44 from IPython.utils.jsonutil import json_clean, encode_images
45 45 from IPython.utils.process import arg_split
46 46 from IPython.utils import py3compat
47 47 from IPython.utils.traitlets import Instance, Type, Dict, CBool, CBytes
48 48 from IPython.utils.warn import error
49 49 from IPython.kernel.zmq.displayhook import ZMQShellDisplayHook
50 50 from IPython.kernel.zmq.datapub import ZMQDataPublisher
51 51 from IPython.kernel.zmq.session import extract_header
52 52 from session import Session
53 53
54 54 #-----------------------------------------------------------------------------
55 55 # Functions and classes
56 56 #-----------------------------------------------------------------------------
57 57
58 58 class ZMQDisplayPublisher(DisplayPublisher):
59 59 """A display publisher that publishes data using a ZeroMQ PUB socket."""
60 60
61 61 session = Instance(Session)
62 62 pub_socket = Instance(SocketABC)
63 63 parent_header = Dict({})
64 64 topic = CBytes(b'display_data')
65 65
66 66 def set_parent(self, parent):
67 67 """Set the parent for outbound messages."""
68 68 self.parent_header = extract_header(parent)
69 69
70 70 def _flush_streams(self):
71 71 """flush IO Streams prior to display"""
72 72 sys.stdout.flush()
73 73 sys.stderr.flush()
74 74
75 75 def publish(self, source, data, metadata=None):
76 76 self._flush_streams()
77 77 if metadata is None:
78 78 metadata = {}
79 79 self._validate_data(source, data, metadata)
80 80 content = {}
81 81 content['source'] = source
82 82 content['data'] = encode_images(data)
83 83 content['metadata'] = metadata
84 84 self.session.send(
85 85 self.pub_socket, u'display_data', json_clean(content),
86 86 parent=self.parent_header, ident=self.topic,
87 87 )
88 88
89 def clear_output(self, stdout=True, stderr=True, other=True):
90 content = dict(stdout=stdout, stderr=stderr, other=other)
91
92 if stdout:
93 print('\r', file=sys.stdout, end='')
94 if stderr:
95 print('\r', file=sys.stderr, end='')
96
89 def clear_output(self):
90 content = {}
91
92 print('\r', file=sys.stdout, end='')
93 print('\r', file=sys.stderr, end='')
97 94 self._flush_streams()
98 95
99 96 self.session.send(
100 97 self.pub_socket, u'clear_output', content,
101 98 parent=self.parent_header, ident=self.topic,
102 99 )
103 100
104 101 @magics_class
105 102 class KernelMagics(Magics):
106 103 #------------------------------------------------------------------------
107 104 # Magic overrides
108 105 #------------------------------------------------------------------------
109 106 # Once the base class stops inheriting from magic, this code needs to be
110 107 # moved into a separate machinery as well. For now, at least isolate here
111 108 # the magics which this class needs to implement differently from the base
112 109 # class, or that are unique to it.
113 110
114 111 @line_magic
115 112 def doctest_mode(self, parameter_s=''):
116 113 """Toggle doctest mode on and off.
117 114
118 115 This mode is intended to make IPython behave as much as possible like a
119 116 plain Python shell, from the perspective of how its prompts, exceptions
120 117 and output look. This makes it easy to copy and paste parts of a
121 118 session into doctests. It does so by:
122 119
123 120 - Changing the prompts to the classic ``>>>`` ones.
124 121 - Changing the exception reporting mode to 'Plain'.
125 122 - Disabling pretty-printing of output.
126 123
127 124 Note that IPython also supports the pasting of code snippets that have
128 125 leading '>>>' and '...' prompts in them. This means that you can paste
129 126 doctests from files or docstrings (even if they have leading
130 127 whitespace), and the code will execute correctly. You can then use
131 128 '%history -t' to see the translated history; this will give you the
132 129 input after removal of all the leading prompts and whitespace, which
133 130 can be pasted back into an editor.
134 131
135 132 With these features, you can switch into this mode easily whenever you
136 133 need to do testing and changes to doctests, without having to leave
137 134 your existing IPython session.
138 135 """
139 136
140 137 from IPython.utils.ipstruct import Struct
141 138
142 139 # Shorthands
143 140 shell = self.shell
144 141 disp_formatter = self.shell.display_formatter
145 142 ptformatter = disp_formatter.formatters['text/plain']
146 143 # dstore is a data store kept in the instance metadata bag to track any
147 144 # changes we make, so we can undo them later.
148 145 dstore = shell.meta.setdefault('doctest_mode', Struct())
149 146 save_dstore = dstore.setdefault
150 147
151 148 # save a few values we'll need to recover later
152 149 mode = save_dstore('mode', False)
153 150 save_dstore('rc_pprint', ptformatter.pprint)
154 151 save_dstore('rc_active_types',disp_formatter.active_types)
155 152 save_dstore('xmode', shell.InteractiveTB.mode)
156 153
157 154 if mode == False:
158 155 # turn on
159 156 ptformatter.pprint = False
160 157 disp_formatter.active_types = ['text/plain']
161 158 shell.magic('xmode Plain')
162 159 else:
163 160 # turn off
164 161 ptformatter.pprint = dstore.rc_pprint
165 162 disp_formatter.active_types = dstore.rc_active_types
166 163 shell.magic("xmode " + dstore.xmode)
167 164
168 165 # Store new mode and inform on console
169 166 dstore.mode = bool(1-int(mode))
170 167 mode_label = ['OFF','ON'][dstore.mode]
171 168 print('Doctest mode is:', mode_label)
172 169
173 170 # Send the payload back so that clients can modify their prompt display
174 171 payload = dict(
175 172 source='doctest_mode',
176 173 mode=dstore.mode)
177 174 shell.payload_manager.write_payload(payload)
178 175
179 176
180 177 _find_edit_target = CodeMagics._find_edit_target
181 178
182 179 @skip_doctest
183 180 @line_magic
184 181 def edit(self, parameter_s='', last_call=['','']):
185 182 """Bring up an editor and execute the resulting code.
186 183
187 184 Usage:
188 185 %edit [options] [args]
189 186
190 187 %edit runs an external text editor. You will need to set the command for
191 188 this editor via the ``TerminalInteractiveShell.editor`` option in your
192 189 configuration file before it will work.
193 190
194 191 This command allows you to conveniently edit multi-line code right in
195 192 your IPython session.
196 193
197 194 If called without arguments, %edit opens up an empty editor with a
198 195 temporary file and will execute the contents of this file when you
199 196 close it (don't forget to save it!).
200 197
201 198
202 199 Options:
203 200
204 201 -n <number>: open the editor at a specified line number. By default,
205 202 the IPython editor hook uses the unix syntax 'editor +N filename', but
206 203 you can configure this by providing your own modified hook if your
207 204 favorite editor supports line-number specifications with a different
208 205 syntax.
209 206
210 207 -p: this will call the editor with the same data as the previous time
211 208 it was used, regardless of how long ago (in your current session) it
212 209 was.
213 210
214 211 -r: use 'raw' input. This option only applies to input taken from the
215 212 user's history. By default, the 'processed' history is used, so that
216 213 magics are loaded in their transformed version to valid Python. If
217 214 this option is given, the raw input as typed as the command line is
218 215 used instead. When you exit the editor, it will be executed by
219 216 IPython's own processor.
220 217
221 218 -x: do not execute the edited code immediately upon exit. This is
222 219 mainly useful if you are editing programs which need to be called with
223 220 command line arguments, which you can then do using %run.
224 221
225 222
226 223 Arguments:
227 224
228 225 If arguments are given, the following possibilites exist:
229 226
230 227 - The arguments are numbers or pairs of colon-separated numbers (like
231 228 1 4:8 9). These are interpreted as lines of previous input to be
232 229 loaded into the editor. The syntax is the same of the %macro command.
233 230
234 231 - If the argument doesn't start with a number, it is evaluated as a
235 232 variable and its contents loaded into the editor. You can thus edit
236 233 any string which contains python code (including the result of
237 234 previous edits).
238 235
239 236 - If the argument is the name of an object (other than a string),
240 237 IPython will try to locate the file where it was defined and open the
241 238 editor at the point where it is defined. You can use `%edit function`
242 239 to load an editor exactly at the point where 'function' is defined,
243 240 edit it and have the file be executed automatically.
244 241
245 242 If the object is a macro (see %macro for details), this opens up your
246 243 specified editor with a temporary file containing the macro's data.
247 244 Upon exit, the macro is reloaded with the contents of the file.
248 245
249 246 Note: opening at an exact line is only supported under Unix, and some
250 247 editors (like kedit and gedit up to Gnome 2.8) do not understand the
251 248 '+NUMBER' parameter necessary for this feature. Good editors like
252 249 (X)Emacs, vi, jed, pico and joe all do.
253 250
254 251 - If the argument is not found as a variable, IPython will look for a
255 252 file with that name (adding .py if necessary) and load it into the
256 253 editor. It will execute its contents with execfile() when you exit,
257 254 loading any code in the file into your interactive namespace.
258 255
259 256 After executing your code, %edit will return as output the code you
260 257 typed in the editor (except when it was an existing file). This way
261 258 you can reload the code in further invocations of %edit as a variable,
262 259 via _<NUMBER> or Out[<NUMBER>], where <NUMBER> is the prompt number of
263 260 the output.
264 261
265 262 Note that %edit is also available through the alias %ed.
266 263
267 264 This is an example of creating a simple function inside the editor and
268 265 then modifying it. First, start up the editor:
269 266
270 267 In [1]: ed
271 268 Editing... done. Executing edited code...
272 269 Out[1]: 'def foo():n print "foo() was defined in an editing session"n'
273 270
274 271 We can then call the function foo():
275 272
276 273 In [2]: foo()
277 274 foo() was defined in an editing session
278 275
279 276 Now we edit foo. IPython automatically loads the editor with the
280 277 (temporary) file where foo() was previously defined:
281 278
282 279 In [3]: ed foo
283 280 Editing... done. Executing edited code...
284 281
285 282 And if we call foo() again we get the modified version:
286 283
287 284 In [4]: foo()
288 285 foo() has now been changed!
289 286
290 287 Here is an example of how to edit a code snippet successive
291 288 times. First we call the editor:
292 289
293 290 In [5]: ed
294 291 Editing... done. Executing edited code...
295 292 hello
296 293 Out[5]: "print 'hello'n"
297 294
298 295 Now we call it again with the previous output (stored in _):
299 296
300 297 In [6]: ed _
301 298 Editing... done. Executing edited code...
302 299 hello world
303 300 Out[6]: "print 'hello world'n"
304 301
305 302 Now we call it with the output #8 (stored in _8, also as Out[8]):
306 303
307 304 In [7]: ed _8
308 305 Editing... done. Executing edited code...
309 306 hello again
310 307 Out[7]: "print 'hello again'n"
311 308 """
312 309
313 310 opts,args = self.parse_options(parameter_s,'prn:')
314 311
315 312 try:
316 313 filename, lineno, _ = CodeMagics._find_edit_target(self.shell, args, opts, last_call)
317 314 except MacroToEdit as e:
318 315 # TODO: Implement macro editing over 2 processes.
319 316 print("Macro editing not yet implemented in 2-process model.")
320 317 return
321 318
322 319 # Make sure we send to the client an absolute path, in case the working
323 320 # directory of client and kernel don't match
324 321 filename = os.path.abspath(filename)
325 322
326 323 payload = {
327 324 'source' : 'edit_magic',
328 325 'filename' : filename,
329 326 'line_number' : lineno
330 327 }
331 328 self.shell.payload_manager.write_payload(payload)
332 329
333 330 # A few magics that are adapted to the specifics of using pexpect and a
334 331 # remote terminal
335 332
336 333 @line_magic
337 334 def clear(self, arg_s):
338 335 """Clear the terminal."""
339 336 if os.name == 'posix':
340 337 self.shell.system("clear")
341 338 else:
342 339 self.shell.system("cls")
343 340
344 341 if os.name == 'nt':
345 342 # This is the usual name in windows
346 343 cls = line_magic('cls')(clear)
347 344
348 345 # Terminal pagers won't work over pexpect, but we do have our own pager
349 346
350 347 @line_magic
351 348 def less(self, arg_s):
352 349 """Show a file through the pager.
353 350
354 351 Files ending in .py are syntax-highlighted."""
355 352 if not arg_s:
356 353 raise UsageError('Missing filename.')
357 354
358 355 cont = open(arg_s).read()
359 356 if arg_s.endswith('.py'):
360 357 cont = self.shell.pycolorize(openpy.read_py_file(arg_s, skip_encoding_cookie=False))
361 358 else:
362 359 cont = open(arg_s).read()
363 360 page.page(cont)
364 361
365 362 more = line_magic('more')(less)
366 363
367 364 # Man calls a pager, so we also need to redefine it
368 365 if os.name == 'posix':
369 366 @line_magic
370 367 def man(self, arg_s):
371 368 """Find the man page for the given command and display in pager."""
372 369 page.page(self.shell.getoutput('man %s | col -b' % arg_s,
373 370 split=False))
374 371
375 372 @line_magic
376 373 def connect_info(self, arg_s):
377 374 """Print information for connecting other clients to this kernel
378 375
379 376 It will print the contents of this session's connection file, as well as
380 377 shortcuts for local clients.
381 378
382 379 In the simplest case, when called from the most recently launched kernel,
383 380 secondary clients can be connected, simply with:
384 381
385 382 $> ipython <app> --existing
386 383
387 384 """
388 385
389 386 from IPython.core.application import BaseIPythonApplication as BaseIPApp
390 387
391 388 if BaseIPApp.initialized():
392 389 app = BaseIPApp.instance()
393 390 security_dir = app.profile_dir.security_dir
394 391 profile = app.profile
395 392 else:
396 393 profile = 'default'
397 394 security_dir = ''
398 395
399 396 try:
400 397 connection_file = get_connection_file()
401 398 info = get_connection_info(unpack=False)
402 399 except Exception as e:
403 400 error("Could not get connection info: %r" % e)
404 401 return
405 402
406 403 # add profile flag for non-default profile
407 404 profile_flag = "--profile %s" % profile if profile != 'default' else ""
408 405
409 406 # if it's in the security dir, truncate to basename
410 407 if security_dir == os.path.dirname(connection_file):
411 408 connection_file = os.path.basename(connection_file)
412 409
413 410
414 411 print (info + '\n')
415 412 print ("Paste the above JSON into a file, and connect with:\n"
416 413 " $> ipython <app> --existing <file>\n"
417 414 "or, if you are local, you can connect with just:\n"
418 415 " $> ipython <app> --existing {0} {1}\n"
419 416 "or even just:\n"
420 417 " $> ipython <app> --existing {1}\n"
421 418 "if this is the most recent IPython session you have started.".format(
422 419 connection_file, profile_flag
423 420 )
424 421 )
425 422
426 423 @line_magic
427 424 def qtconsole(self, arg_s):
428 425 """Open a qtconsole connected to this kernel.
429 426
430 427 Useful for connecting a qtconsole to running notebooks, for better
431 428 debugging.
432 429 """
433 430
434 431 # %qtconsole should imply bind_kernel for engines:
435 432 try:
436 433 from IPython.parallel import bind_kernel
437 434 except ImportError:
438 435 # technically possible, because parallel has higher pyzmq min-version
439 436 pass
440 437 else:
441 438 bind_kernel()
442 439
443 440 try:
444 441 p = connect_qtconsole(argv=arg_split(arg_s, os.name=='posix'))
445 442 except Exception as e:
446 443 error("Could not start qtconsole: %r" % e)
447 444 return
448 445
449 446 @line_magic
450 447 def autosave(self, arg_s):
451 448 """Set the autosave interval in the notebook (in seconds).
452 449
453 450 The default value is 120, or two minutes.
454 451 ``%autosave 0`` will disable autosave.
455 452
456 453 This magic only has an effect when called from the notebook interface.
457 454 It has no effect when called in a startup file.
458 455 """
459 456
460 457 try:
461 458 interval = int(arg_s)
462 459 except ValueError:
463 460 raise UsageError("%%autosave requires an integer, got %r" % arg_s)
464 461
465 462 # javascript wants milliseconds
466 463 milliseconds = 1000 * interval
467 464 display(Javascript("IPython.notebook.set_autosave_interval(%i)" % milliseconds),
468 465 include=['application/javascript']
469 466 )
470 467 if interval:
471 468 print("Autosaving every %i seconds" % interval)
472 469 else:
473 470 print("Autosave disabled")
474 471
475 472
476 473 class ZMQInteractiveShell(InteractiveShell):
477 474 """A subclass of InteractiveShell for ZMQ."""
478 475
479 476 displayhook_class = Type(ZMQShellDisplayHook)
480 477 display_pub_class = Type(ZMQDisplayPublisher)
481 478 data_pub_class = Type(ZMQDataPublisher)
482 479
483 480 # Override the traitlet in the parent class, because there's no point using
484 481 # readline for the kernel. Can be removed when the readline code is moved
485 482 # to the terminal frontend.
486 483 colors_force = CBool(True)
487 484 readline_use = CBool(False)
488 485 # autoindent has no meaning in a zmqshell, and attempting to enable it
489 486 # will print a warning in the absence of readline.
490 487 autoindent = CBool(False)
491 488
492 489 exiter = Instance(ZMQExitAutocall)
493 490 def _exiter_default(self):
494 491 return ZMQExitAutocall(self)
495 492
496 493 def _exit_now_changed(self, name, old, new):
497 494 """stop eventloop when exit_now fires"""
498 495 if new:
499 496 loop = ioloop.IOLoop.instance()
500 497 loop.add_timeout(time.time()+0.1, loop.stop)
501 498
502 499 keepkernel_on_exit = None
503 500
504 501 # Over ZeroMQ, GUI control isn't done with PyOS_InputHook as there is no
505 502 # interactive input being read; we provide event loop support in ipkernel
506 503 @staticmethod
507 504 def enable_gui(gui):
508 505 from .eventloops import enable_gui as real_enable_gui
509 506 try:
510 507 real_enable_gui(gui)
511 508 except ValueError as e:
512 509 raise UsageError("%s" % e)
513 510
514 511 def init_environment(self):
515 512 """Configure the user's environment.
516 513
517 514 """
518 515 env = os.environ
519 516 # These two ensure 'ls' produces nice coloring on BSD-derived systems
520 517 env['TERM'] = 'xterm-color'
521 518 env['CLICOLOR'] = '1'
522 519 # Since normal pagers don't work at all (over pexpect we don't have
523 520 # single-key control of the subprocess), try to disable paging in
524 521 # subprocesses as much as possible.
525 522 env['PAGER'] = 'cat'
526 523 env['GIT_PAGER'] = 'cat'
527 524
528 525 # And install the payload version of page.
529 526 install_payload_page()
530 527
531 528 def auto_rewrite_input(self, cmd):
532 529 """Called to show the auto-rewritten input for autocall and friends.
533 530
534 531 FIXME: this payload is currently not correctly processed by the
535 532 frontend.
536 533 """
537 534 new = self.prompt_manager.render('rewrite') + cmd
538 535 payload = dict(
539 536 source='auto_rewrite_input',
540 537 transformed_input=new,
541 538 )
542 539 self.payload_manager.write_payload(payload)
543 540
544 541 def ask_exit(self):
545 542 """Engage the exit actions."""
546 543 self.exit_now = True
547 544 payload = dict(
548 545 source='ask_exit',
549 546 exit=True,
550 547 keepkernel=self.keepkernel_on_exit,
551 548 )
552 549 self.payload_manager.write_payload(payload)
553 550
554 551 def _showtraceback(self, etype, evalue, stb):
555 552
556 553 exc_content = {
557 554 u'traceback' : stb,
558 555 u'ename' : unicode(etype.__name__),
559 556 u'evalue' : py3compat.safe_unicode(evalue),
560 557 }
561 558
562 559 dh = self.displayhook
563 560 # Send exception info over pub socket for other clients than the caller
564 561 # to pick up
565 562 topic = None
566 563 if dh.topic:
567 564 topic = dh.topic.replace(b'pyout', b'pyerr')
568 565
569 566 exc_msg = dh.session.send(dh.pub_socket, u'pyerr', json_clean(exc_content), dh.parent_header, ident=topic)
570 567
571 568 # FIXME - Hack: store exception info in shell object. Right now, the
572 569 # caller is reading this info after the fact, we need to fix this logic
573 570 # to remove this hack. Even uglier, we need to store the error status
574 571 # here, because in the main loop, the logic that sets it is being
575 572 # skipped because runlines swallows the exceptions.
576 573 exc_content[u'status'] = u'error'
577 574 self._reply_content = exc_content
578 575 # /FIXME
579 576
580 577 return exc_content
581 578
582 579 def set_next_input(self, text):
583 580 """Send the specified text to the frontend to be presented at the next
584 581 input cell."""
585 582 payload = dict(
586 583 source='set_next_input',
587 584 text=text
588 585 )
589 586 self.payload_manager.write_payload(payload)
590 587
591 588 #-------------------------------------------------------------------------
592 589 # Things related to magics
593 590 #-------------------------------------------------------------------------
594 591
595 592 def init_magics(self):
596 593 super(ZMQInteractiveShell, self).init_magics()
597 594 self.register_magics(KernelMagics)
598 595 self.magics_manager.register_alias('ed', 'edit')
599 596
600 597
601 598
602 599 InteractiveShellABC.register(ZMQInteractiveShell)
@@ -1,172 +1,172 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2008-2012 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // EngineInteract
10 10 //============================================================================
11 11
12 12 var key = IPython.utils.keycodes;
13 13
14 14
15 15 var DirectViewWidget = function (selector, kernel, targets) {
16 16 // The kernel doesn't have to be set at creation time, in that case
17 17 // it will be null and set_kernel has to be called later.
18 18 this.selector = selector;
19 19 this.element = $(selector);
20 20 this.kernel = kernel || null;
21 21 this.code_mirror = null;
22 22 this.targets = targets;
23 23 this.create_element();
24 24 };
25 25
26 26
27 27 DirectViewWidget.prototype.create_element = function () {
28 28 this.element.addClass('cell border-box-sizing code_cell vbox');
29 29 this.element.attr('tabindex','2');
30 30 this.element.css('padding-right',0);
31 31
32 32 var control = $('<div/>').addClass('dv_control').height('30px');
33 33 var control_label = $('<span/>').html('Select engine(s) to run code on interactively: ');
34 34 control_label.css('line-height','30px');
35 35 var select = $('<select/>').addClass('dv_select ui-widget ui-widget-content');
36 36 select.css('font-size','85%%').css('margin-bottom','5px');
37 37 var n = this.targets.length;
38 38 select.append($('<option/>').html('all').attr('value','all'));
39 39 for (var i=0; i<n; i++) {
40 40 select.append($('<option/>').html(this.targets[i]).attr('value',this.targets[i]))
41 41 }
42 42 control.append(control_label).append(select);
43 43
44 44 var input = $('<div></div>').addClass('input hbox');
45 45 var input_area = $('<div/>').addClass('input_area box-flex1');
46 46 this.code_mirror = CodeMirror(input_area.get(0), {
47 47 indentUnit : 4,
48 48 mode: 'python',
49 49 theme: 'ipython',
50 50 onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this)
51 51 });
52 52 input.append(input_area);
53 53 var output = $('<div></div>');
54 54
55 55
56 56 this.element.append(control).append(input).append(output);
57 57 this.output_area = new IPython.OutputArea(output, false);
58 58
59 59 };
60 60
61 61
62 62 DirectViewWidget.prototype.handle_codemirror_keyevent = function (editor, event) {
63 63 // This method gets called in CodeMirror's onKeyDown/onKeyPress
64 64 // handlers and is used to provide custom key handling. Its return
65 65 // value is used to determine if CodeMirror should ignore the event:
66 66 // true = ignore, false = don't ignore.
67 67
68 68 var that = this;
69 69 var cur = editor.getCursor();
70 70
71 71 if (event.keyCode === key.ENTER && event.shiftKey && event.type === 'keydown') {
72 72 // Always ignore shift-enter in CodeMirror as we handle it.
73 73 event.stop();
74 74 that.execute();
75 75 return true;
76 76 } else if (event.keyCode === key.UP && event.type === 'keydown') {
77 77 event.stop();
78 78 return false;
79 79 } else if (event.keyCode === key.DOWN && event.type === 'keydown') {
80 80 event.stop();
81 81 return false;
82 82 } else if (event.keyCode === key.BACKSPACE && event.type == 'keydown') {
83 83 // If backspace and the line ends with 4 spaces, remove them.
84 84 var line = editor.getLine(cur.line);
85 85 var ending = line.slice(-4);
86 86 if (ending === ' ') {
87 87 editor.replaceRange('',
88 88 {line: cur.line, ch: cur.ch-4},
89 89 {line: cur.line, ch: cur.ch}
90 90 );
91 91 event.stop();
92 92 return true;
93 93 } else {
94 94 return false;
95 95 };
96 96 };
97 97
98 98 return false;
99 99 };
100 100
101 101
102 102 // Kernel related calls.
103 103
104 104
105 105 DirectViewWidget.prototype.set_kernel = function (kernel) {
106 106 this.kernel = kernel;
107 107 }
108 108
109 109
110 110 DirectViewWidget.prototype.execute = function () {
111 this.output_area.clear_output(true, true, true);
111 this.output_area.clear_output();
112 112 this.element.addClass("running");
113 113 var callbacks = {
114 114 'execute_reply': $.proxy(this._handle_execute_reply, this),
115 115 'output': $.proxy(this.output_area.handle_output, this.output_area),
116 116 'clear_output': $.proxy(this.output_area.handle_clear_output, this.output_area),
117 117 };
118 118 var target = this.element.find('.dv_select option:selected').attr('value');
119 119 if (target === 'all') {
120 120 target = '"all"';
121 121 }
122 122 var code = '%(widget_var)s.execute("""'+this.get_text()+'""",targets='+target+')';
123 123 var msg_id = this.kernel.execute(code, callbacks, {silent: false});
124 124 this.clear_input();
125 125 this.code_mirror.focus();
126 126 };
127 127
128 128
129 129 DirectViewWidget.prototype._handle_execute_reply = function (content) {
130 130 this.element.removeClass("running");
131 131 // this.dirty = true;
132 132 }
133 133
134 134 // Basic cell manipulation.
135 135
136 136
137 137 DirectViewWidget.prototype.select_all = function () {
138 138 var start = {line: 0, ch: 0};
139 139 var nlines = this.code_mirror.lineCount();
140 140 var last_line = this.code_mirror.getLine(nlines-1);
141 141 var end = {line: nlines-1, ch: last_line.length};
142 142 this.code_mirror.setSelection(start, end);
143 143 };
144 144
145 145
146 146 DirectViewWidget.prototype.clear_input = function () {
147 147 this.code_mirror.setValue('');
148 148 };
149 149
150 150
151 151 DirectViewWidget.prototype.get_text = function () {
152 152 return this.code_mirror.getValue();
153 153 };
154 154
155 155
156 156 DirectViewWidget.prototype.set_text = function (code) {
157 157 return this.code_mirror.setValue(code);
158 158 };
159 159
160 160 container.show();
161 161 var widget = $('<div/>')
162 162 // When templating over a JSON string, we must use single quotes.
163 163 var targets = '%(targets)s';
164 164 targets = $.parseJSON(targets);
165 165 var eiw = new DirectViewWidget(widget, IPython.notebook.kernel, targets);
166 166 element.append(widget);
167 167 element.css('padding',0);
168 168 setTimeout(function () {
169 169 eiw.code_mirror.refresh();
170 170 eiw.code_mirror.focus();
171 171 }, 1);
172 172
General Comments 0
You need to be logged in to leave comments. Login now