##// END OF EJS Templates
Misc work on shortcuts:...
Brian E. Granger -
Show More
@@ -1,683 +1,709 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 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 // Keyboard management
10 10 //============================================================================
11 11
12 12 var IPython = (function (IPython) {
13 13 "use strict";
14 14
15 15 // Setup global keycodes and inverse keycodes.
16 16
17 17 // See http://unixpapa.com/js/key.html for a complete description. The short of
18 18 // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes"
19 19 // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same
20 20 // but have minor differences.
21 21
22 22 // These apply to Firefox, (Webkit and IE)
23 23 var _keycodes = {
24 24 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73,
25 25 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82,
26 26 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90,
27 27 '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54,
28 28 '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48,
29 29 '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191,
30 30 '\\ |': 220, '\' "': 222,
31 31 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100,
32 32 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105,
33 33 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111,
34 34 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118,
35 35 'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126,
36 36 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18,
37 37 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34,
38 38 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40,
39 39 'insert': 45, 'delete': 46, 'numlock': 144,
40 40 };
41 41
42 42 // These apply to Firefox and Opera
43 43 var _mozilla_keycodes = {
44 44 '; :': 59, '= +': 61, '- _': 173, 'meta': 224
45 45 }
46 46
47 47 // This apply to Webkit and IE
48 48 var _ie_keycodes = {
49 49 '; :': 186, '= +': 187, '- _': 189,
50 50 }
51 51
52 52 var browser = IPython.utils.browser[0];
53 53
54 54 if (browser === 'Firefox' || browser === 'Opera') {
55 55 $.extend(_keycodes, _mozilla_keycodes);
56 56 } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') {
57 57 $.extend(_keycodes, _ie_keycodes);
58 58 }
59 59
60 60 var keycodes = {};
61 61 var inv_keycodes = {};
62 62 for (var name in _keycodes) {
63 63 var names = name.split(' ');
64 64 if (names.length === 1) {
65 65 var n = names[0]
66 66 keycodes[n] = _keycodes[n]
67 67 inv_keycodes[_keycodes[n]] = n
68 68 } else {
69 69 var primary = names[0];
70 70 var secondary = names[1];
71 71 keycodes[primary] = _keycodes[name]
72 72 keycodes[secondary] = _keycodes[name]
73 73 inv_keycodes[_keycodes[name]] = primary
74 74 }
75 75 }
76 76
77 77
78 78 // Default keyboard shortcuts
79 79
80 80 var default_common_shortcuts = {
81 81 'meta+s' : {
82 82 help : 'save notebook',
83 83 help_index : 'fb',
84 84 handler : function (event) {
85 85 IPython.notebook.save_checkpoint();
86 86 event.preventDefault();
87 87 return false;
88 88 }
89 89 },
90 90 'ctrl+s' : {
91 91 help : 'save notebook',
92 92 help_index : 'fc',
93 93 handler : function (event) {
94 94 IPython.notebook.save_checkpoint();
95 95 event.preventDefault();
96 96 return false;
97 97 }
98 98 },
99 99 'shift' : {
100 100 help : '',
101 101 help_index : '',
102 102 handler : function (event) {
103 103 // ignore shift keydown
104 104 return true;
105 105 }
106 106 },
107 107 'shift+enter' : {
108 108 help : 'run cell',
109 109 help_index : 'ba',
110 110 handler : function (event) {
111 111 IPython.notebook.execute_cell();
112 112 return false;
113 113 }
114 114 },
115 115 'ctrl+enter' : {
116 116 help : 'run cell, select below',
117 117 help_index : 'bb',
118 118 handler : function (event) {
119 119 IPython.notebook.execute_cell_and_select_below();
120 120 return false;
121 121 }
122 122 },
123 123 'alt+enter' : {
124 124 help : 'run cell, insert below',
125 125 help_index : 'bc',
126 126 handler : function (event) {
127 127 IPython.notebook.execute_cell_and_insert_below();
128 128 return false;
129 129 }
130 130 }
131 131 }
132 132
133 133 // Edit mode defaults
134 134
135 135 var default_edit_shortcuts = {
136 136 'esc' : {
137 137 help : 'command mode',
138 138 help_index : 'aa',
139 139 handler : function (event) {
140 140 IPython.notebook.command_mode();
141 141 IPython.notebook.focus_cell();
142 142 return false;
143 143 }
144 144 },
145 145 'ctrl+m' : {
146 146 help : 'command mode',
147 147 help_index : 'ab',
148 148 handler : function (event) {
149 149 IPython.notebook.command_mode();
150 150 IPython.notebook.focus_cell();
151 151 return false;
152 152 }
153 153 },
154 154 'up' : {
155 155 help : '',
156 156 help_index : '',
157 157 handler : function (event) {
158 158 var cell = IPython.notebook.get_selected_cell();
159 159 if (cell && cell.at_top()) {
160 160 event.preventDefault();
161 161 IPython.notebook.command_mode()
162 162 IPython.notebook.select_prev();
163 163 IPython.notebook.edit_mode();
164 164 return false;
165 165 };
166 166 }
167 167 },
168 168 'down' : {
169 169 help : '',
170 170 help_index : '',
171 171 handler : function (event) {
172 172 var cell = IPython.notebook.get_selected_cell();
173 173 if (cell && cell.at_bottom()) {
174 174 event.preventDefault();
175 175 IPython.notebook.command_mode()
176 176 IPython.notebook.select_next();
177 177 IPython.notebook.edit_mode();
178 178 return false;
179 179 };
180 180 }
181 181 },
182 182 'alt+-' : {
183 183 help : 'split cell',
184 184 help_index : 'ea',
185 185 handler : function (event) {
186 186 IPython.notebook.split_cell();
187 187 return false;
188 188 }
189 189 },
190 190 'alt+subtract' : {
191 191 help : '',
192 192 help_index : 'eb',
193 193 handler : function (event) {
194 194 IPython.notebook.split_cell();
195 195 return false;
196 196 }
197 197 },
198 198 }
199 199
200 200 // Command mode defaults
201 201
202 202 var default_command_shortcuts = {
203 203 'enter' : {
204 204 help : 'edit mode',
205 205 help_index : 'aa',
206 206 handler : function (event) {
207 207 IPython.notebook.edit_mode();
208 208 return false;
209 209 }
210 210 },
211 211 'up' : {
212 212 help : 'select previous cell',
213 213 help_index : 'da',
214 214 handler : function (event) {
215 215 var index = IPython.notebook.get_selected_index();
216 216 if (index !== 0 && index !== null) {
217 217 IPython.notebook.select_prev();
218 218 var cell = IPython.notebook.get_selected_cell();
219 219 cell.focus_cell();
220 220 };
221 221 return false;
222 222 }
223 223 },
224 224 'down' : {
225 225 help : 'select next cell',
226 226 help_index : 'db',
227 227 handler : function (event) {
228 228 var index = IPython.notebook.get_selected_index();
229 229 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
230 230 IPython.notebook.select_next();
231 231 var cell = IPython.notebook.get_selected_cell();
232 232 cell.focus_cell();
233 233 };
234 234 return false;
235 235 }
236 236 },
237 237 'k' : {
238 238 help : 'select previous cell',
239 239 help_index : 'dc',
240 240 handler : function (event) {
241 241 var index = IPython.notebook.get_selected_index();
242 242 if (index !== 0 && index !== null) {
243 243 IPython.notebook.select_prev();
244 244 var cell = IPython.notebook.get_selected_cell();
245 245 cell.focus_cell();
246 246 };
247 247 return false;
248 248 }
249 249 },
250 250 'j' : {
251 251 help : 'select next cell',
252 252 help_index : 'dd',
253 253 handler : function (event) {
254 254 var index = IPython.notebook.get_selected_index();
255 255 if (index !== (IPython.notebook.ncells()-1) && index !== null) {
256 256 IPython.notebook.select_next();
257 257 var cell = IPython.notebook.get_selected_cell();
258 258 cell.focus_cell();
259 259 };
260 260 return false;
261 261 }
262 262 },
263 263 'x' : {
264 264 help : 'cut cell',
265 265 help_index : 'ee',
266 266 handler : function (event) {
267 267 IPython.notebook.cut_cell();
268 268 return false;
269 269 }
270 270 },
271 271 'c' : {
272 272 help : 'copy cell',
273 273 help_index : 'ef',
274 274 handler : function (event) {
275 275 IPython.notebook.copy_cell();
276 276 return false;
277 277 }
278 278 },
279 'shift+v' : {
280 help : 'paste cell above',
281 help_index : 'eg',
282 handler : function (event) {
283 IPython.notebook.paste_cell_above();
284 return false;
285 }
286 },
279 287 'v' : {
280 288 help : 'paste cell below',
281 help_index : 'eg',
289 help_index : 'eh',
282 290 handler : function (event) {
283 291 IPython.notebook.paste_cell_below();
284 292 return false;
285 293 }
286 294 },
287 295 'd' : {
288 296 help : 'delete cell (press twice)',
289 help_index : 'ei',
297 help_index : 'ej',
298 count: 2,
290 299 handler : function (event) {
291 var dc = IPython.keyboard_manager._delete_count;
292 if (dc === 0) {
293 IPython.keyboard_manager._delete_count = 1;
294 setTimeout(function () {
295 IPython.keyboard_manager._delete_count = 0;
296 }, 800);
297 } else if (dc === 1) {
298 300 IPython.notebook.delete_cell();
299 IPython.keyboard_manager._delete_count = 0;
300 }
301 301 return false;
302 302 }
303 303 },
304 304 'a' : {
305 305 help : 'insert cell above',
306 306 help_index : 'ec',
307 307 handler : function (event) {
308 308 IPython.notebook.insert_cell_above('code');
309 309 IPython.notebook.select_prev();
310 310 IPython.notebook.focus_cell();
311 311 return false;
312 312 }
313 313 },
314 314 'b' : {
315 315 help : 'insert cell below',
316 316 help_index : 'ed',
317 317 handler : function (event) {
318 318 IPython.notebook.insert_cell_below('code');
319 319 IPython.notebook.select_next();
320 320 IPython.notebook.focus_cell();
321 321 return false;
322 322 }
323 323 },
324 324 'y' : {
325 325 help : 'to code',
326 326 help_index : 'ca',
327 327 handler : function (event) {
328 328 IPython.notebook.to_code();
329 329 return false;
330 330 }
331 331 },
332 332 'm' : {
333 333 help : 'to markdown',
334 334 help_index : 'cb',
335 335 handler : function (event) {
336 336 IPython.notebook.to_markdown();
337 337 return false;
338 338 }
339 339 },
340 't' : {
340 'r' : {
341 341 help : 'to raw',
342 342 help_index : 'cc',
343 343 handler : function (event) {
344 344 IPython.notebook.to_raw();
345 345 return false;
346 346 }
347 347 },
348 348 '1' : {
349 349 help : 'to heading 1',
350 350 help_index : 'cd',
351 351 handler : function (event) {
352 352 IPython.notebook.to_heading(undefined, 1);
353 353 return false;
354 354 }
355 355 },
356 356 '2' : {
357 357 help : 'to heading 2',
358 358 help_index : 'ce',
359 359 handler : function (event) {
360 360 IPython.notebook.to_heading(undefined, 2);
361 361 return false;
362 362 }
363 363 },
364 364 '3' : {
365 365 help : 'to heading 3',
366 366 help_index : 'cf',
367 367 handler : function (event) {
368 368 IPython.notebook.to_heading(undefined, 3);
369 369 return false;
370 370 }
371 371 },
372 372 '4' : {
373 373 help : 'to heading 4',
374 374 help_index : 'cg',
375 375 handler : function (event) {
376 376 IPython.notebook.to_heading(undefined, 4);
377 377 return false;
378 378 }
379 379 },
380 380 '5' : {
381 381 help : 'to heading 5',
382 382 help_index : 'ch',
383 383 handler : function (event) {
384 384 IPython.notebook.to_heading(undefined, 5);
385 385 return false;
386 386 }
387 387 },
388 388 '6' : {
389 389 help : 'to heading 6',
390 390 help_index : 'ci',
391 391 handler : function (event) {
392 392 IPython.notebook.to_heading(undefined, 6);
393 393 return false;
394 394 }
395 395 },
396 396 'o' : {
397 397 help : 'toggle output',
398 398 help_index : 'gb',
399 399 handler : function (event) {
400 400 IPython.notebook.toggle_output();
401 401 return false;
402 402 }
403 403 },
404 404 'shift+o' : {
405 405 help : 'toggle output',
406 406 help_index : 'gc',
407 407 handler : function (event) {
408 408 IPython.notebook.toggle_output_scroll();
409 409 return false;
410 410 }
411 411 },
412 412 's' : {
413 413 help : 'save notebook',
414 414 help_index : 'fa',
415 415 handler : function (event) {
416 416 IPython.notebook.save_checkpoint();
417 417 return false;
418 418 }
419 419 },
420 420 'ctrl+j' : {
421 421 help : 'move cell down',
422 422 help_index : 'eb',
423 423 handler : function (event) {
424 424 IPython.notebook.move_cell_down();
425 425 return false;
426 426 }
427 427 },
428 428 'ctrl+k' : {
429 429 help : 'move cell up',
430 430 help_index : 'ea',
431 431 handler : function (event) {
432 432 IPython.notebook.move_cell_up();
433 433 return false;
434 434 }
435 435 },
436 436 'l' : {
437 437 help : 'toggle line numbers',
438 438 help_index : 'ga',
439 439 handler : function (event) {
440 440 IPython.notebook.cell_toggle_line_numbers();
441 441 return false;
442 442 }
443 443 },
444 444 'i' : {
445 help : 'interrupt kernel',
445 help : 'interrupt kernel (press twice)',
446 446 help_index : 'ha',
447 count: 2,
447 448 handler : function (event) {
448 449 IPython.notebook.kernel.interrupt();
449 450 return false;
450 451 }
451 452 },
452 '.' : {
453 help : 'restart kernel',
453 '0' : {
454 help : 'restart kernel (press twice)',
454 455 help_index : 'hb',
456 count: 2,
455 457 handler : function (event) {
456 458 IPython.notebook.restart_kernel();
457 459 return false;
458 460 }
459 461 },
460 462 'h' : {
461 463 help : 'keyboard shortcuts',
462 464 help_index : 'gd',
463 465 handler : function (event) {
464 466 IPython.quick_help.show_keyboard_shortcuts();
465 467 return false;
466 468 }
467 469 },
468 470 'z' : {
469 471 help : 'undo last delete',
470 help_index : 'eh',
472 help_index : 'ei',
471 473 handler : function (event) {
472 474 IPython.notebook.undelete_cell();
473 475 return false;
474 476 }
475 477 },
476 478 'shift+=' : {
477 479 help : 'merge cell below',
478 help_index : 'ej',
480 help_index : 'ek',
479 481 handler : function (event) {
480 482 IPython.notebook.merge_cell_below();
481 483 return false;
482 484 }
483 485 },
484 486 }
485 487
486 488
487 489 // Shortcut manager class
488 490
489 var ShortcutManager = function () {
491 var ShortcutManager = function (delay) {
490 492 this._shortcuts = {}
493 this._counts = {}
494 this.delay = delay || 800; // delay in milliseconds
491 495 }
492 496
493 497 ShortcutManager.prototype.help = function () {
494 498 var help = [];
495 499 for (var shortcut in this._shortcuts) {
496 500 var help_string = this._shortcuts[shortcut]['help'];
497 501 var help_index = this._shortcuts[shortcut]['help_index'];
498 502 if (help_string) {
499 503 help.push({
500 504 shortcut: shortcut,
501 505 help: help_string,
502 506 help_index: help_index}
503 507 );
504 508 }
505 509 }
506 510 help.sort(function (a, b) {
507 511 if (a.help_index > b.help_index)
508 512 return 1;
509 513 if (a.help_index < b.help_index)
510 514 return -1;
511 515 return 0;
512 516 });
513 517 return help;
514 518 }
515 519
516 520 ShortcutManager.prototype.normalize_key = function (key) {
517 521 return inv_keycodes[keycodes[key]];
518 522 }
519 523
520 524 ShortcutManager.prototype.normalize_shortcut = function (shortcut) {
521 525 // Sort a sequence of + separated modifiers into the order alt+ctrl+meta+shift
522 526 var values = shortcut.split("+");
523 527 if (values.length === 1) {
524 528 return this.normalize_key(values[0])
525 529 } else {
526 530 var modifiers = values.slice(0,-1);
527 531 var key = this.normalize_key(values[values.length-1]);
528 532 modifiers.sort();
529 533 return modifiers.join('+') + '+' + key;
530 534 }
531 535 }
532 536
533 537 ShortcutManager.prototype.event_to_shortcut = function (event) {
534 538 // Convert a jQuery keyboard event to a strong based keyboard shortcut
535 539 var shortcut = '';
536 540 var key = inv_keycodes[event.which]
537 541 if (event.altKey && key !== 'alt') {shortcut += 'alt+';}
538 542 if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl+';}
539 543 if (event.metaKey && key !== 'meta') {shortcut += 'meta+';}
540 544 if (event.shiftKey && key !== 'shift') {shortcut += 'shift+';}
541 545 shortcut += key;
542 546 return shortcut
543 547 }
544 548
545 549 ShortcutManager.prototype.clear_shortcuts = function () {
546 550 this._shortcuts = {};
547 551 }
548 552
549 553 ShortcutManager.prototype.add_shortcut = function (shortcut, data) {
550 554 if (typeof(data) === 'function') {
551 555 data = {help: '', help_index: '', handler: data}
552 556 }
553 557 data.help_index = data.help_index || '';
554 558 data.help = data.help || '';
559 data.count = data.count || 1;
555 560 if (data.help_index === '') {
556 561 data.help_index = 'zz';
557 562 }
558 563 shortcut = this.normalize_shortcut(shortcut);
564 this._counts[shortcut] = 0;
559 565 this._shortcuts[shortcut] = data;
560 566 }
561 567
562 568 ShortcutManager.prototype.add_shortcuts = function (data) {
563 569 for (var shortcut in data) {
564 570 this.add_shortcut(shortcut, data[shortcut]);
565 571 }
566 572 }
567 573
568 574 ShortcutManager.prototype.remove_shortcut = function (shortcut) {
569 575 shortcut = this.normalize_shortcut(shortcut);
576 delete this._counts[shortcut];
570 577 delete this._shortcuts[shortcut];
571 578 }
572 579
580 ShortcutManager.prototype.count_handler = function (shortcut, event, handler) {
581 var that = this;
582 var c = this._counts;
583 if (c[shortcut] === 0) {
584 c[shortcut] = 1;
585 setTimeout(function () {
586 c[shortcut] = 0;
587 }, that.delay);
588 } else if (c[shortcut] === 1) {
589 c[shortcut] = 0;
590 return handler(event);
591 }
592 return false;
593
594 }
595
573 596 ShortcutManager.prototype.call_handler = function (event) {
574 597 var shortcut = this.event_to_shortcut(event);
575 598 var data = this._shortcuts[shortcut];
576 if (data !== undefined) {
599 if (data) {
577 600 var handler = data['handler'];
578 if (handler !== undefined) {
601 if (handler) {
602 if (data.count === 1) {
579 603 return handler(event);
604 } else if (data.count > 1) {
605 return this.count_handler(shortcut, event, handler);
606 }
580 607 }
581 608 }
582 609 return true;
583 610 }
584 611
585 612
586 613
587 614 // Main keyboard manager for the notebook
588 615
589 616 var KeyboardManager = function () {
590 617 this.mode = 'command';
591 618 this.enabled = true;
592 this._delete_count = 0;
593 619 this.bind_events();
594 620 this.command_shortcuts = new ShortcutManager();
595 621 this.command_shortcuts.add_shortcuts(default_common_shortcuts);
596 622 this.command_shortcuts.add_shortcuts(default_command_shortcuts);
597 623 this.edit_shortcuts = new ShortcutManager();
598 624 this.edit_shortcuts.add_shortcuts(default_common_shortcuts);
599 625 this.edit_shortcuts.add_shortcuts(default_edit_shortcuts);
600 626 };
601 627
602 628 KeyboardManager.prototype.bind_events = function () {
603 629 var that = this;
604 630 $(document).keydown(function (event) {
605 631 return that.handle_keydown(event);
606 632 });
607 633 };
608 634
609 635 KeyboardManager.prototype.handle_keydown = function (event) {
610 636 var notebook = IPython.notebook;
611 637
612 638 if (event.which === keycodes['esc']) {
613 639 // Intercept escape at highest level to avoid closing
614 640 // websocket connection with firefox
615 641 event.preventDefault();
616 642 }
617 643
618 644 if (!this.enabled) {
619 645 if (event.which === keycodes['esc']) {
620 646 // ESC
621 647 notebook.command_mode();
622 648 return false;
623 649 }
624 650 return true;
625 651 }
626 652
627 653 if (this.mode === 'edit') {
628 654 return this.edit_shortcuts.call_handler(event);
629 655 } else if (this.mode === 'command') {
630 656 return this.command_shortcuts.call_handler(event);
631 657 }
632 658 return true;
633 659 }
634 660
635 661 KeyboardManager.prototype.edit_mode = function () {
636 662 this.last_mode = this.mode;
637 663 this.mode = 'edit';
638 664 }
639 665
640 666 KeyboardManager.prototype.command_mode = function () {
641 667 this.last_mode = this.mode;
642 668 this.mode = 'command';
643 669 }
644 670
645 671 KeyboardManager.prototype.enable = function () {
646 672 this.enabled = true;
647 673 }
648 674
649 675 KeyboardManager.prototype.disable = function () {
650 676 this.enabled = false;
651 677 }
652 678
653 679 KeyboardManager.prototype.register_events = function (e) {
654 680 var that = this;
655 681 e.on('focusin', function () {
656 682 that.command_mode();
657 683 that.disable();
658 684 });
659 685 e.on('focusout', function () {
660 686 that.command_mode();
661 687 that.enable();
662 688 });
663 689 // There are times (raw_input) where we remove the element from the DOM before
664 690 // focusout is called. In this case we bind to the remove event of jQueryUI,
665 691 // which gets triggered upon removal.
666 692 e.on('remove', function () {
667 693 that.command_mode();
668 694 that.enable();
669 695 });
670 696 }
671 697
672 698
673 699 IPython.keycodes = keycodes;
674 700 IPython.inv_keycodes = inv_keycodes;
675 701 IPython.default_common_shortcuts = default_common_shortcuts;
676 702 IPython.default_edit_shortcuts = default_edit_shortcuts;
677 703 IPython.default_command_shortcuts = default_command_shortcuts;
678 704 IPython.ShortcutManager = ShortcutManager;
679 705 IPython.KeyboardManager = KeyboardManager;
680 706
681 707 return IPython;
682 708
683 709 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now