diff --git a/IPython/html/static/base/js/dialog.js b/IPython/html/static/base/js/dialog.js index 7fbcd7c..350bd2a 100644 --- a/IPython/html/static/base/js/dialog.js +++ b/IPython/html/static/base/js/dialog.js @@ -65,13 +65,17 @@ IPython.dialog = (function (IPython) { dialog.remove(); }); } - if (options.reselect_cell !== false) { - dialog.on("hidden", function () { - if (IPython.notebook) { - var cell = IPython.notebook.get_selected_cell(); - if (cell) cell.select(); - } - }); + dialog.on("hidden", function () { + if (IPython.notebook) { + var cell = IPython.notebook.get_selected_cell(); + if (cell) cell.select(); + IPython.keyboard_manager.enable(); + IPython.keyboard_manager.command_mode(); + } + }); + + if (IPython.keyboard_manager) { + IPython.keyboard_manager.disable(); } return dialog.modal(options); diff --git a/IPython/html/static/base/js/utils.js b/IPython/html/static/base/js/utils.js index aa4dcbe..b3f7b3a 100644 --- a/IPython/html/static/base/js/utils.js +++ b/IPython/html/static/base/js/utils.js @@ -455,6 +455,26 @@ IPython.utils = (function (IPython) { return M; })(); + var is_or_has = function (a, b) { + // Is b a child of a or a itself? + return a.has(b).length !==0 || a.is(b); + } + + var is_focused = function (e) { + // Is element e, or one of its children focused? + e = $(e); + var target = $(document.activeElement); + if (target.length > 0) { + if (is_or_has(e, target)) { + return true; + } else { + return false; + } + } else { + return false; + } + } + return { regex_split : regex_split, @@ -475,7 +495,9 @@ IPython.utils = (function (IPython) { encode_uri_components : encode_uri_components, splitext : splitext, always_new : always_new, - browser : browser + browser : browser, + is_or_has : is_or_has, + is_focused : is_focused }; }(IPython)); diff --git a/IPython/html/static/notebook/js/cell.js b/IPython/html/static/notebook/js/cell.js index 502df10..636f61e 100644 --- a/IPython/html/static/notebook/js/cell.js +++ b/IPython/html/static/notebook/js/cell.js @@ -39,6 +39,8 @@ var IPython = (function (IPython) { this.placeholder = options.placeholder || ''; this.read_only = options.cm_config.readOnly; this.selected = false; + this.rendered = false; + this.mode = 'command'; this.metadata = {}; // load this from metadata later ? this.user_highlight = 'auto'; @@ -60,6 +62,7 @@ var IPython = (function (IPython) { if (this.element !== null) { this.element.data("cell", this); this.bind_events(); + this.init_classes(); } }; @@ -97,6 +100,26 @@ var IPython = (function (IPython) { Cell.prototype.create_element = function () { }; + Cell.prototype.init_classes = function () { + // Call after this.element exists to initialize the css classes + // related to selected, rendered and mode. + if (this.selected) { + this.element.addClass('selected'); + } else { + this.element.addClass('unselected'); + } + if (this.rendered) { + this.element.addClass('rendered'); + } else { + this.element.addClass('unrendered'); + } + if (this.mode === 'edit') { + this.element.addClass('edit_mode'); + } else { + this.element.addClass('command_mode'); + } + } + /** * Subclasses can implement override bind_events. @@ -108,20 +131,41 @@ var IPython = (function (IPython) { var that = this; // We trigger events so that Cell doesn't have to depend on Notebook. that.element.click(function (event) { - if (that.selected === false) { + if (!that.selected) { $([IPython.events]).trigger('select.Cell', {'cell':that}); - } + }; }); that.element.focusin(function (event) { - if (that.selected === false) { + if (!that.selected) { $([IPython.events]).trigger('select.Cell', {'cell':that}); - } + }; }); if (this.code_mirror) { this.code_mirror.on("change", function(cm, change) { $([IPython.events]).trigger("set_dirty.Notebook", {value: true}); }); } + if (this.code_mirror) { + this.code_mirror.on('focus', function(cm, change) { + $([IPython.events]).trigger('edit_mode.Cell', {cell: that}); + }); + } + if (this.code_mirror) { + this.code_mirror.on('blur', function(cm, change) { + if (that.mode === 'edit') { + setTimeout(function () { + var isf = IPython.utils.is_focused; + var trigger = true; + if (isf('div#tooltip') || isf('div.completions')) { + trigger = false; + } + if (trigger) { + $([IPython.events]).trigger('command_mode.Cell', {cell: that}); + } + }, 1); + } + }); + } }; /** @@ -129,47 +173,126 @@ var IPython = (function (IPython) { * @method typeset */ Cell.prototype.typeset = function () { - if (window.MathJax){ + if (window.MathJax) { var cell_math = this.element.get(0); MathJax.Hub.Queue(["Typeset", MathJax.Hub, cell_math]); } }; /** - * should be triggerd when cell is selected + * handle cell level logic when a cell is selected * @method select + * @return is the action being taken */ Cell.prototype.select = function () { - this.element.addClass('selected'); - this.selected = true; + if (!this.selected) { + this.element.addClass('selected'); + this.element.removeClass('unselected'); + this.selected = true; + return true; + } else { + return false; + } }; - /** - * should be triggerd when cell is unselected + * handle cell level logic when a cell is unselected * @method unselect + * @return is the action being taken */ Cell.prototype.unselect = function () { - this.element.removeClass('selected'); - this.selected = false; + if (this.selected) { + this.element.addClass('unselected'); + this.element.removeClass('selected'); + this.selected = false; + return true; + } else { + return false; + } }; /** - * should be overritten by subclass - * @method get_text + * handle cell level logic when a cell is rendered + * @method render + * @return is the action being taken */ - Cell.prototype.get_text = function () { + Cell.prototype.render = function () { + if (!this.rendered) { + this.element.addClass('rendered'); + this.element.removeClass('unrendered'); + this.rendered = true; + return true; + } else { + return false; + } }; /** - * should be overritten by subclass - * @method set_text - * @param {string} text + * handle cell level logic when a cell is unrendered + * @method unrender + * @return is the action being taken */ - Cell.prototype.set_text = function (text) { + Cell.prototype.unrender = function () { + if (this.rendered) { + this.element.addClass('unrendered'); + this.element.removeClass('rendered'); + this.rendered = false; + return true; + } else { + return false; + } + }; + + /** + * enter the command mode for the cell + * @method command_mode + * @return is the action being taken + */ + Cell.prototype.command_mode = function () { + if (this.mode !== 'command') { + this.element.addClass('command_mode'); + this.element.removeClass('edit_mode'); + this.mode = 'command'; + return true; + } else { + return false; + } }; /** + * enter the edit mode for the cell + * @method command_mode + * @return is the action being taken + */ + Cell.prototype.edit_mode = function () { + if (this.mode !== 'edit') { + this.element.addClass('edit_mode'); + this.element.removeClass('command_mode'); + this.mode = 'edit'; + return true; + } else { + return false; + } + } + + /** + * Focus the cell in the DOM sense + * @method focus_cell + */ + Cell.prototype.focus_cell = function () { + this.element.focus(); + } + + /** + * Focus the editor area so a user can type + * @method focus_editor + */ + Cell.prototype.focus_editor = function () { + this.refresh(); + this.code_mirror.focus(); + } + + /** * Refresh codemirror instance * @method refresh */ @@ -177,20 +300,19 @@ var IPython = (function (IPython) { this.code_mirror.refresh(); }; - /** * should be overritten by subclass - * @method edit - **/ - Cell.prototype.edit = function () { + * @method get_text + */ + Cell.prototype.get_text = function () { }; - /** * should be overritten by subclass - * @method render - **/ - Cell.prototype.render = function () { + * @method set_text + * @param {string} text + */ + Cell.prototype.set_text = function (text) { }; /** diff --git a/IPython/html/static/notebook/js/codecell.js b/IPython/html/static/notebook/js/codecell.js index 132b3ab..c21a136 100644 --- a/IPython/html/static/notebook/js/codecell.js +++ b/IPython/html/static/notebook/js/codecell.js @@ -74,7 +74,7 @@ var IPython = (function (IPython) { var cm_overwrite_options = { - onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this) + onKeyEvent: $.proxy(this.handle_keyevent,this) }; options = this.mergeopt(CodeCell, options, {cm_config:cm_overwrite_options}); @@ -139,6 +139,27 @@ var IPython = (function (IPython) { this.completer = new IPython.Completer(this); }; + /** @method bind_events */ + CodeCell.prototype.bind_events = function () { + IPython.Cell.prototype.bind_events.apply(this); + var that = this; + + this.element.focusout( + function() { that.auto_highlight(); } + ); + }; + + CodeCell.prototype.handle_keyevent = function (editor, event) { + + // console.log('CM', this.mode, event.which, event.type) + + if (this.mode === 'command') { + return true; + } else if (this.mode === 'edit') { + return this.handle_codemirror_keyevent(editor, event); + } + }; + /** * This method gets called in CodeMirror's onKeyDown/onKeyPress * handlers and is used to provide custom key handling. Its return @@ -151,8 +172,9 @@ var IPython = (function (IPython) { var that = this; // whatever key is pressed, first, cancel the tooltip request before // they are sent, and remove tooltip if any, except for tab again + var tooltip_closed = null; if (event.type === 'keydown' && event.which != key.TAB ) { - IPython.tooltip.remove_and_cancel_tooltip(); + tooltip_closed = IPython.tooltip.remove_and_cancel_tooltip(); } var cur = editor.getCursor(); @@ -160,7 +182,7 @@ var IPython = (function (IPython) { this.auto_highlight(); } - if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey)) { + if (event.keyCode === key.ENTER && (event.shiftKey || event.ctrlKey || event.altKey)) { // Always ignore shift-enter in CodeMirror as we handle it. return true; } else if (event.which === 40 && event.type === 'keypress' && IPython.tooltip.time_before_tooltip >= 0) { @@ -179,8 +201,32 @@ var IPython = (function (IPython) { } else { return true; } - } else if (event.which === key.ESC) { - return IPython.tooltip.remove_and_cancel_tooltip(true); + } else if (event.which === key.ESC && event.type === 'keydown') { + // First see if the tooltip is active and if so cancel it. + if (tooltip_closed) { + // The call to remove_and_cancel_tooltip above in L177 doesn't pass + // force=true. Because of this it won't actually close the tooltip + // if it is in sticky mode. Thus, we have to check again if it is open + // and close it with force=true. + if (!IPython.tooltip._hidden) { + IPython.tooltip.remove_and_cancel_tooltip(true); + } + // If we closed the tooltip, don't let CM or the global handlers + // handle this event. + event.stop(); + return true; + } + if (that.code_mirror.options.keyMap === "vim-insert") { + // vim keyMap is active and in insert mode. In this case we leave vim + // insert mode, but remain in notebook edit mode. + // Let' CM handle this event and prevent global handling. + event.stop(); + return false; + } else { + // vim keyMap is not active. Leave notebook edit mode. + // Don't let CM handle the event, defer to global handling. + return true; + } } else if (event.which === key.DOWNARROW && event.type === 'keydown') { // If we are not at the bottom, let CM handle the down arrow and // prevent the global keydown handler from handling it. @@ -190,7 +236,7 @@ var IPython = (function (IPython) { } else { return true; } - } else if (event.keyCode === key.TAB && event.type == 'keydown' && event.shiftKey) { + } else if (event.keyCode === key.TAB && event.type === 'keydown' && event.shiftKey) { if (editor.somethingSelected()){ var anchor = editor.getCursor("anchor"); var head = editor.getCursor("head"); @@ -225,7 +271,6 @@ var IPython = (function (IPython) { return false; }; - // Kernel related calls. CodeCell.prototype.set_kernel = function (kernel) { @@ -304,15 +349,32 @@ var IPython = (function (IPython) { // Basic cell manipulation. CodeCell.prototype.select = function () { - IPython.Cell.prototype.select.apply(this); - this.code_mirror.refresh(); - this.code_mirror.focus(); - this.auto_highlight(); - // We used to need an additional refresh() after the focus, but - // it appears that this has been fixed in CM. This bug would show - // up on FF when a newly loaded markdown cell was edited. + var cont = IPython.Cell.prototype.select.apply(this); + if (cont) { + this.code_mirror.refresh(); + this.auto_highlight(); + } + return cont; + }; + + CodeCell.prototype.render = function () { + var cont = IPython.Cell.prototype.render.apply(this); + // Always execute, even if we are already in the rendered state + return cont; + }; + + CodeCell.prototype.unrender = function () { + // CodeCell is always rendered + return false; }; + CodeCell.prototype.edit_mode = function () { + var cont = IPython.Cell.prototype.edit_mode.apply(this); + if (cont) { + this.focus_editor(); + } + return cont; + } CodeCell.prototype.select_all = function () { var start = {line: 0, ch: 0}; diff --git a/IPython/html/static/notebook/js/completer.js b/IPython/html/static/notebook/js/completer.js index cfb2e90..e3e4617 100644 --- a/IPython/html/static/notebook/js/completer.js +++ b/IPython/html/static/notebook/js/completer.js @@ -218,6 +218,8 @@ var IPython = (function (IPython) { this.complete = $('
').addClass('completions'); this.complete.attr('id', 'complete'); + // Currently webkit doesn't use the size attr correctly. See: + // https://code.google.com/p/chromium/issues/detail?id=4579 this.sel = $('') .attr('multiple', 'true') .attr('size', Math.min(10, this.raw_result.length)); @@ -255,6 +257,7 @@ var IPython = (function (IPython) { this.build_gui_list(this.raw_result); this.sel.focus(); + IPython.keyboard_manager.disable(); // Opera sometimes ignores focusing a freshly created node if (window.opera) setTimeout(function () { if (!this.done) this.sel.focus(); @@ -279,6 +282,7 @@ var IPython = (function (IPython) { if (this.done) return; this.done = true; $('.completions').remove(); + IPython.keyboard_manager.enable(); } Completer.prototype.pick = function () { diff --git a/IPython/html/static/notebook/js/keyboardmanager.js b/IPython/html/static/notebook/js/keyboardmanager.js new file mode 100644 index 0000000..f2a1b39 --- /dev/null +++ b/IPython/html/static/notebook/js/keyboardmanager.js @@ -0,0 +1,683 @@ +//---------------------------------------------------------------------------- +// Copyright (C) 2011 The IPython Development Team +// +// Distributed under the terms of the BSD License. The full license is in +// the file COPYING, distributed as part of this software. +//---------------------------------------------------------------------------- + +//============================================================================ +// Keyboard management +//============================================================================ + +var IPython = (function (IPython) { + "use strict"; + + // Setup global keycodes and inverse keycodes. + + // See http://unixpapa.com/js/key.html for a complete description. The short of + // it is that there are different keycode sets. Firefox uses the "Mozilla keycodes" + // and Webkit/IE use the "IE keycodes". These keycode sets are mostly the same + // but have minor differences. + + // These apply to Firefox, (Webkit and IE) + var _keycodes = { + 'a': 65, 'b': 66, 'c': 67, 'd': 68, 'e': 69, 'f': 70, 'g': 71, 'h': 72, 'i': 73, + 'j': 74, 'k': 75, 'l': 76, 'm': 77, 'n': 78, 'o': 79, 'p': 80, 'q': 81, 'r': 82, + 's': 83, 't': 84, 'u': 85, 'v': 86, 'w': 87, 'x': 88, 'y': 89, 'z': 90, + '1 !': 49, '2 @': 50, '3 #': 51, '4 $': 52, '5 %': 53, '6 ^': 54, + '7 &': 55, '8 *': 56, '9 (': 57, '0 )': 48, + '[ {': 219, '] }': 221, '` ~': 192, ', <': 188, '. >': 190, '/ ?': 191, + '\\ |': 220, '\' "': 222, + 'numpad0': 96, 'numpad1': 97, 'numpad2': 98, 'numpad3': 99, 'numpad4': 100, + 'numpad5': 101, 'numpad6': 102, 'numpad7': 103, 'numpad8': 104, 'numpad9': 105, + 'multiply': 106, 'add': 107, 'subtract': 109, 'decimal': 110, 'divide': 111, + 'f1': 112, 'f2': 113, 'f3': 114, 'f4': 115, 'f5': 116, 'f6': 117, 'f7': 118, + 'f8': 119, 'f9': 120, 'f11': 122, 'f12': 123, 'f13': 124, 'f14': 125, 'f15': 126, + 'backspace': 8, 'tab': 9, 'enter': 13, 'shift': 16, 'ctrl': 17, 'alt': 18, + 'meta': 91, 'capslock': 20, 'esc': 27, 'space': 32, 'pageup': 33, 'pagedown': 34, + 'end': 35, 'home': 36, 'left': 37, 'up': 38, 'right': 39, 'down': 40, + 'insert': 45, 'delete': 46, 'numlock': 144, + }; + + // These apply to Firefox and Opera + var _mozilla_keycodes = { + '; :': 59, '= +': 61, '- _': 173, 'meta': 224 + } + + // This apply to Webkit and IE + var _ie_keycodes = { + '; :': 186, '= +': 187, '- _': 189, + } + + var browser = IPython.utils.browser[0]; + + if (browser === 'Firefox' || browser === 'Opera') { + $.extend(_keycodes, _mozilla_keycodes); + } else if (browser === 'Safari' || browser === 'Chrome' || browser === 'MSIE') { + $.extend(_keycodes, _ie_keycodes); + } + + var keycodes = {}; + var inv_keycodes = {}; + for (var name in _keycodes) { + var names = name.split(' '); + if (names.length === 1) { + var n = names[0] + keycodes[n] = _keycodes[n] + inv_keycodes[_keycodes[n]] = n + } else { + var primary = names[0]; + var secondary = names[1]; + keycodes[primary] = _keycodes[name] + keycodes[secondary] = _keycodes[name] + inv_keycodes[_keycodes[name]] = primary + } + } + + + // Default keyboard shortcuts + + var default_common_shortcuts = { + 'meta+s' : { + help : 'save notebook', + help_index : 'fb', + handler : function (event) { + IPython.notebook.save_checkpoint(); + event.preventDefault(); + return false; + } + }, + 'ctrl+s' : { + help : 'save notebook', + help_index : 'fc', + handler : function (event) { + IPython.notebook.save_checkpoint(); + event.preventDefault(); + return false; + } + }, + 'shift' : { + help : '', + help_index : '', + handler : function (event) { + // ignore shift keydown + return true; + } + }, + 'shift+enter' : { + help : 'run cell', + help_index : 'ba', + handler : function (event) { + IPython.notebook.execute_cell(); + return false; + } + }, + 'ctrl+enter' : { + help : 'run cell, select below', + help_index : 'bb', + handler : function (event) { + IPython.notebook.execute_cell_and_select_below(); + return false; + } + }, + 'alt+enter' : { + help : 'run cell, insert below', + help_index : 'bc', + handler : function (event) { + IPython.notebook.execute_cell_and_insert_below(); + return false; + } + } + } + + // Edit mode defaults + + var default_edit_shortcuts = { + 'esc' : { + help : 'command mode', + help_index : 'aa', + handler : function (event) { + IPython.notebook.command_mode(); + IPython.notebook.focus_cell(); + return false; + } + }, + 'ctrl+m' : { + help : 'command mode', + help_index : 'ab', + handler : function (event) { + IPython.notebook.command_mode(); + IPython.notebook.focus_cell(); + return false; + } + }, + 'up' : { + help : '', + help_index : '', + handler : function (event) { + var cell = IPython.notebook.get_selected_cell(); + if (cell && cell.at_top()) { + event.preventDefault(); + IPython.notebook.command_mode() + IPython.notebook.select_prev(); + IPython.notebook.edit_mode(); + return false; + }; + } + }, + 'down' : { + help : '', + help_index : '', + handler : function (event) { + var cell = IPython.notebook.get_selected_cell(); + if (cell && cell.at_bottom()) { + event.preventDefault(); + IPython.notebook.command_mode() + IPython.notebook.select_next(); + IPython.notebook.edit_mode(); + return false; + }; + } + }, + 'alt+-' : { + help : 'split cell', + help_index : 'ea', + handler : function (event) { + IPython.notebook.split_cell(); + return false; + } + }, + 'alt+subtract' : { + help : '', + help_index : 'eb', + handler : function (event) { + IPython.notebook.split_cell(); + return false; + } + }, + } + + // Command mode defaults + + var default_command_shortcuts = { + 'enter' : { + help : 'edit mode', + help_index : 'aa', + handler : function (event) { + IPython.notebook.edit_mode(); + return false; + } + }, + 'up' : { + help : 'select previous cell', + help_index : 'da', + handler : function (event) { + var index = IPython.notebook.get_selected_index(); + if (index !== 0 && index !== null) { + IPython.notebook.select_prev(); + var cell = IPython.notebook.get_selected_cell(); + cell.focus_cell(); + }; + return false; + } + }, + 'down' : { + help : 'select next cell', + help_index : 'db', + handler : function (event) { + var index = IPython.notebook.get_selected_index(); + if (index !== (IPython.notebook.ncells()-1) && index !== null) { + IPython.notebook.select_next(); + var cell = IPython.notebook.get_selected_cell(); + cell.focus_cell(); + }; + return false; + } + }, + 'k' : { + help : 'select previous cell', + help_index : 'dc', + handler : function (event) { + var index = IPython.notebook.get_selected_index(); + if (index !== 0 && index !== null) { + IPython.notebook.select_prev(); + var cell = IPython.notebook.get_selected_cell(); + cell.focus_cell(); + }; + return false; + } + }, + 'j' : { + help : 'select next cell', + help_index : 'dd', + handler : function (event) { + var index = IPython.notebook.get_selected_index(); + if (index !== (IPython.notebook.ncells()-1) && index !== null) { + IPython.notebook.select_next(); + var cell = IPython.notebook.get_selected_cell(); + cell.focus_cell(); + }; + return false; + } + }, + 'x' : { + help : 'cut cell', + help_index : 'ee', + handler : function (event) { + IPython.notebook.cut_cell(); + return false; + } + }, + 'c' : { + help : 'copy cell', + help_index : 'ef', + handler : function (event) { + IPython.notebook.copy_cell(); + return false; + } + }, + 'v' : { + help : 'paste cell below', + help_index : 'eg', + handler : function (event) { + IPython.notebook.paste_cell_below(); + return false; + } + }, + 'd' : { + help : 'delete cell (press twice)', + help_index : 'ei', + handler : function (event) { + var dc = IPython.keyboard_manager._delete_count; + if (dc === 0) { + IPython.keyboard_manager._delete_count = 1; + setTimeout(function () { + IPython.keyboard_manager._delete_count = 0; + }, 800); + } else if (dc === 1) { + IPython.notebook.delete_cell(); + IPython.keyboard_manager._delete_count = 0; + } + return false; + } + }, + 'a' : { + help : 'insert cell above', + help_index : 'ec', + handler : function (event) { + IPython.notebook.insert_cell_above('code'); + IPython.notebook.select_prev(); + IPython.notebook.focus_cell(); + return false; + } + }, + 'b' : { + help : 'insert cell below', + help_index : 'ed', + handler : function (event) { + IPython.notebook.insert_cell_below('code'); + IPython.notebook.select_next(); + IPython.notebook.focus_cell(); + return false; + } + }, + 'y' : { + help : 'to code', + help_index : 'ca', + handler : function (event) { + IPython.notebook.to_code(); + return false; + } + }, + 'm' : { + help : 'to markdown', + help_index : 'cb', + handler : function (event) { + IPython.notebook.to_markdown(); + return false; + } + }, + 't' : { + help : 'to raw', + help_index : 'cc', + handler : function (event) { + IPython.notebook.to_raw(); + return false; + } + }, + '1' : { + help : 'to heading 1', + help_index : 'cd', + handler : function (event) { + IPython.notebook.to_heading(undefined, 1); + return false; + } + }, + '2' : { + help : 'to heading 2', + help_index : 'ce', + handler : function (event) { + IPython.notebook.to_heading(undefined, 2); + return false; + } + }, + '3' : { + help : 'to heading 3', + help_index : 'cf', + handler : function (event) { + IPython.notebook.to_heading(undefined, 3); + return false; + } + }, + '4' : { + help : 'to heading 4', + help_index : 'cg', + handler : function (event) { + IPython.notebook.to_heading(undefined, 4); + return false; + } + }, + '5' : { + help : 'to heading 5', + help_index : 'ch', + handler : function (event) { + IPython.notebook.to_heading(undefined, 5); + return false; + } + }, + '6' : { + help : 'to heading 6', + help_index : 'ci', + handler : function (event) { + IPython.notebook.to_heading(undefined, 6); + return false; + } + }, + 'o' : { + help : 'toggle output', + help_index : 'gb', + handler : function (event) { + IPython.notebook.toggle_output(); + return false; + } + }, + 'shift+o' : { + help : 'toggle output', + help_index : 'gc', + handler : function (event) { + IPython.notebook.toggle_output_scroll(); + return false; + } + }, + 's' : { + help : 'save notebook', + help_index : 'fa', + handler : function (event) { + IPython.notebook.save_checkpoint(); + return false; + } + }, + 'ctrl+j' : { + help : 'move cell down', + help_index : 'eb', + handler : function (event) { + IPython.notebook.move_cell_down(); + return false; + } + }, + 'ctrl+k' : { + help : 'move cell up', + help_index : 'ea', + handler : function (event) { + IPython.notebook.move_cell_up(); + return false; + } + }, + 'l' : { + help : 'toggle line numbers', + help_index : 'ga', + handler : function (event) { + IPython.notebook.cell_toggle_line_numbers(); + return false; + } + }, + 'i' : { + help : 'interrupt kernel', + help_index : 'ha', + handler : function (event) { + IPython.notebook.kernel.interrupt(); + return false; + } + }, + '.' : { + help : 'restart kernel', + help_index : 'hb', + handler : function (event) { + IPython.notebook.restart_kernel(); + return false; + } + }, + 'h' : { + help : 'keyboard shortcuts', + help_index : 'gd', + handler : function (event) { + IPython.quick_help.show_keyboard_shortcuts(); + return false; + } + }, + 'z' : { + help : 'undo last delete', + help_index : 'eh', + handler : function (event) { + IPython.notebook.undelete_cell(); + return false; + } + }, + 'shift+=' : { + help : 'merge cell below', + help_index : 'ej', + handler : function (event) { + IPython.notebook.merge_cell_below(); + return false; + } + }, + } + + + // Shortcut manager class + + var ShortcutManager = function () { + this._shortcuts = {} + } + + ShortcutManager.prototype.help = function () { + var help = []; + for (var shortcut in this._shortcuts) { + var help_string = this._shortcuts[shortcut]['help']; + var help_index = this._shortcuts[shortcut]['help_index']; + if (help_string) { + help.push({ + shortcut: shortcut, + help: help_string, + help_index: help_index} + ); + } + } + help.sort(function (a, b) { + if (a.help_index > b.help_index) + return 1; + if (a.help_index < b.help_index) + return -1; + return 0; + }); + return help; + } + + ShortcutManager.prototype.normalize_key = function (key) { + return inv_keycodes[keycodes[key]]; + } + + ShortcutManager.prototype.normalize_shortcut = function (shortcut) { + // Sort a sequence of + separated modifiers into the order alt+ctrl+meta+shift + var values = shortcut.split("+"); + if (values.length === 1) { + return this.normalize_key(values[0]) + } else { + var modifiers = values.slice(0,-1); + var key = this.normalize_key(values[values.length-1]); + modifiers.sort(); + return modifiers.join('+') + '+' + key; + } + } + + ShortcutManager.prototype.event_to_shortcut = function (event) { + // Convert a jQuery keyboard event to a strong based keyboard shortcut + var shortcut = ''; + var key = inv_keycodes[event.which] + if (event.altKey && key !== 'alt') {shortcut += 'alt+';} + if (event.ctrlKey && key !== 'ctrl') {shortcut += 'ctrl+';} + if (event.metaKey && key !== 'meta') {shortcut += 'meta+';} + if (event.shiftKey && key !== 'shift') {shortcut += 'shift+';} + shortcut += key; + return shortcut + } + + ShortcutManager.prototype.clear_shortcuts = function () { + this._shortcuts = {}; + } + + ShortcutManager.prototype.add_shortcut = function (shortcut, data) { + if (typeof(data) === 'function') { + data = {help: '', help_index: '', handler: data} + } + data.help_index = data.help_index || ''; + data.help = data.help || ''; + if (data.help_index === '') { + data.help_index = 'zz'; + } + shortcut = this.normalize_shortcut(shortcut); + this._shortcuts[shortcut] = data; + } + + ShortcutManager.prototype.add_shortcuts = function (data) { + for (var shortcut in data) { + this.add_shortcut(shortcut, data[shortcut]); + } + } + + ShortcutManager.prototype.remove_shortcut = function (shortcut) { + shortcut = this.normalize_shortcut(shortcut); + delete this._shortcuts[shortcut]; + } + + ShortcutManager.prototype.call_handler = function (event) { + var shortcut = this.event_to_shortcut(event); + var data = this._shortcuts[shortcut]; + if (data !== undefined) { + var handler = data['handler']; + if (handler !== undefined) { + return handler(event); + } + } + return true; + } + + + + // Main keyboard manager for the notebook + + var KeyboardManager = function () { + this.mode = 'command'; + this.enabled = true; + this._delete_count = 0; + this.bind_events(); + this.command_shortcuts = new ShortcutManager(); + this.command_shortcuts.add_shortcuts(default_common_shortcuts); + this.command_shortcuts.add_shortcuts(default_command_shortcuts); + this.edit_shortcuts = new ShortcutManager(); + this.edit_shortcuts.add_shortcuts(default_common_shortcuts); + this.edit_shortcuts.add_shortcuts(default_edit_shortcuts); + }; + + KeyboardManager.prototype.bind_events = function () { + var that = this; + $(document).keydown(function (event) { + return that.handle_keydown(event); + }); + }; + + KeyboardManager.prototype.handle_keydown = function (event) { + var notebook = IPython.notebook; + + if (event.which === keycodes['esc']) { + // Intercept escape at highest level to avoid closing + // websocket connection with firefox + event.preventDefault(); + } + + if (!this.enabled) { + if (event.which === keycodes['esc']) { + // ESC + notebook.command_mode(); + return false; + } + return true; + } + + if (this.mode === 'edit') { + return this.edit_shortcuts.call_handler(event); + } else if (this.mode === 'command') { + return this.command_shortcuts.call_handler(event); + } + return true; + } + + KeyboardManager.prototype.edit_mode = function () { + this.last_mode = this.mode; + this.mode = 'edit'; + } + + KeyboardManager.prototype.command_mode = function () { + this.last_mode = this.mode; + this.mode = 'command'; + } + + KeyboardManager.prototype.enable = function () { + this.enabled = true; + } + + KeyboardManager.prototype.disable = function () { + this.enabled = false; + } + + KeyboardManager.prototype.register_events = function (e) { + var that = this; + e.on('focusin', function () { + that.command_mode(); + that.disable(); + }); + e.on('focusout', function () { + that.command_mode(); + that.enable(); + }); + // There are times (raw_input) where we remove the element from the DOM before + // focusout is called. In this case we bind to the remove event of jQueryUI, + // which gets triggered upon removal. + e.on('remove', function () { + that.command_mode(); + that.enable(); + }); + } + + + IPython.keycodes = keycodes; + IPython.inv_keycodes = inv_keycodes; + IPython.default_common_shortcuts = default_common_shortcuts; + IPython.default_edit_shortcuts = default_edit_shortcuts; + IPython.default_command_shortcuts = default_command_shortcuts; + IPython.ShortcutManager = ShortcutManager; + IPython.KeyboardManager = KeyboardManager; + + return IPython; + +}(IPython)); diff --git a/IPython/html/static/notebook/js/main.js b/IPython/html/static/notebook/js/main.js index e0ac198..0caeaec 100644 --- a/IPython/html/static/notebook/js/main.js +++ b/IPython/html/static/notebook/js/main.js @@ -62,6 +62,7 @@ function (marked) { IPython.quick_help = new IPython.QuickHelp(); IPython.login_widget = new IPython.LoginWidget('span#login_widget',{baseProjectUrl:baseProjectUrl}); IPython.notebook = new IPython.Notebook('div#notebook',{baseProjectUrl:baseProjectUrl, notebookPath:notebookPath, notebookName:notebookName}); + IPython.keyboard_manager = new IPython.KeyboardManager(); IPython.save_widget = new IPython.SaveWidget('span#save_widget'); IPython.menubar = new IPython.MenuBar('#menubar',{baseProjectUrl:baseProjectUrl, notebookPath: notebookPath}) IPython.toolbar = new IPython.MainToolBar('#maintoolbar-container') diff --git a/IPython/html/static/notebook/js/maintoolbar.js b/IPython/html/static/notebook/js/maintoolbar.js index 58e51f7..9c09e37 100644 --- a/IPython/html/static/notebook/js/maintoolbar.js +++ b/IPython/html/static/notebook/js/maintoolbar.js @@ -41,6 +41,8 @@ var IPython = (function (IPython) { icon : 'icon-plus-sign', callback : function () { IPython.notebook.insert_cell_below('code'); + IPython.notebook.select_next(); + IPython.notebook.focus_cell(); } } ],'insert_above_below'); @@ -98,7 +100,7 @@ var IPython = (function (IPython) { label : 'Run Cell', icon : 'icon-play', callback : function () { - IPython.notebook.execute_selected_cell(); + IPython.notebook.execute_cell(); } }, { diff --git a/IPython/html/static/notebook/js/menubar.js b/IPython/html/static/notebook/js/menubar.js index b4e8fec..e531eef 100644 --- a/IPython/html/static/notebook/js/menubar.js +++ b/IPython/html/static/notebook/js/menubar.js @@ -161,7 +161,7 @@ var IPython = (function (IPython) { IPython.notebook.delete_cell(); }); this.element.find('#undelete_cell').click(function () { - IPython.notebook.undelete(); + IPython.notebook.undelete_cell(); }); this.element.find('#split_cell').click(function () { IPython.notebook.split_cell(); @@ -200,16 +200,21 @@ var IPython = (function (IPython) { // Insert this.element.find('#insert_cell_above').click(function () { IPython.notebook.insert_cell_above('code'); + IPython.notebook.select_prev(); }); this.element.find('#insert_cell_below').click(function () { IPython.notebook.insert_cell_below('code'); + IPython.notebook.select_next(); }); // Cell this.element.find('#run_cell').click(function () { - IPython.notebook.execute_selected_cell(); + IPython.notebook.execute_cell(); + }); + this.element.find('#run_cell_select_below').click(function () { + IPython.notebook.execute_cell_and_select_below(); }); - this.element.find('#run_cell_in_place').click(function () { - IPython.notebook.execute_selected_cell({terminal:true}); + this.element.find('#run_cell_insert_below').click(function () { + IPython.notebook.execute_cell_and_insert_below(); }); this.element.find('#run_all_cells').click(function () { IPython.notebook.execute_all_cells(); diff --git a/IPython/html/static/notebook/js/notebook.js b/IPython/html/static/notebook/js/notebook.js index e396b12..6cc0745 100644 --- a/IPython/html/static/notebook/js/notebook.js +++ b/IPython/html/static/notebook/js/notebook.js @@ -13,7 +13,6 @@ var IPython = (function (IPython) { "use strict"; var utils = IPython.utils; - var key = IPython.utils.keycodes; /** * A notebook contains and manages cells. @@ -39,6 +38,9 @@ var IPython = (function (IPython) { this.undelete_index = null; this.undelete_below = false; this.paste_enabled = false; + // It is important to start out in command mode to match the intial mode + // of the KeyboardManager. + this.mode = 'command'; this.set_dirty(false); this.metadata = {}; this._checkpoint_after_save = false; @@ -50,7 +52,6 @@ var IPython = (function (IPython) { this.minimum_autosave_interval = 120000; // single worksheet for now this.worksheet_metadata = {}; - this.control_key_active = false; this.notebook_name_blacklist_re = /[\/\\:]/; this.nbformat = 3 // Increment this when changing the nbformat this.nbformat_minor = 0 // Increment this when changing the nbformat @@ -74,7 +75,7 @@ var IPython = (function (IPython) { * @method baseProjectUrl * @return {String} The base project URL */ - Notebook.prototype.baseProjectUrl = function(){ + Notebook.prototype.baseProjectUrl = function() { return this._baseProjectUrl || $('body').data('baseProjectUrl'); }; @@ -92,12 +93,13 @@ var IPython = (function (IPython) { * @method create_elements */ Notebook.prototype.create_elements = function () { + var that = this; + this.element.attr('tabindex','-1'); + this.container = $("").addClass("container").attr("id", "notebook-container"); // We add this end_space div to the end of the notebook div to: // i) provide a margin between the last cell and the end of the notebook // ii) to prevent the div from scrolling up when the last cell is being // edited, but is too low on the page, which browsers will do automatically. - var that = this; - this.container = $("").addClass("container").attr("id", "notebook-container"); var end_space = $('').addClass('end_space'); end_space.dblclick(function (e) { var ncells = that.ncells(); @@ -105,7 +107,6 @@ var IPython = (function (IPython) { }); this.element.append(this.container); this.container.append(end_space); - $('div#notebook').addClass('border-box-sizing'); }; /** @@ -131,7 +132,17 @@ var IPython = (function (IPython) { var index = that.find_cell_index(data.cell); that.select(index); }); - + + $([IPython.events]).on('edit_mode.Cell', function (event, data) { + var index = that.find_cell_index(data.cell); + that.select(index); + that.edit_mode(); + }); + + $([IPython.events]).on('command_mode.Cell', function (event, data) { + that.command_mode(); + }); + $([IPython.events]).on('status_autorestarting.Kernel', function () { IPython.dialog.modal({ title: "Kernel Restarting", @@ -144,220 +155,25 @@ var IPython = (function (IPython) { }); }); - - $(document).keydown(function (event) { - - // Save (CTRL+S) or (AppleKey+S) - //metaKey = applekey on mac - if ((event.ctrlKey || event.metaKey) && event.keyCode==83) { - that.save_checkpoint(); - event.preventDefault(); - return false; - } else if (event.which === key.ESC) { - // Intercept escape at highest level to avoid closing - // websocket connection with firefox - IPython.pager.collapse(); - event.preventDefault(); - } else if (event.which === key.SHIFT) { - // ignore shift keydown - return true; - } - if (event.which === key.UPARROW && !event.shiftKey) { - var cell = that.get_selected_cell(); - if (cell && cell.at_top()) { - event.preventDefault(); - that.select_prev(); - }; - } else if (event.which === key.DOWNARROW && !event.shiftKey) { - var cell = that.get_selected_cell(); - if (cell && cell.at_bottom()) { - event.preventDefault(); - that.select_next(); - }; - } else if (event.which === key.ENTER && event.shiftKey) { - that.execute_selected_cell(); - return false; - } else if (event.which === key.ENTER && event.altKey) { - // Execute code cell, and insert new in place - that.execute_selected_cell(); - // Only insert a new cell, if we ended up in an already populated cell - if (/\S/.test(that.get_selected_cell().get_text()) == true) { - that.insert_cell_above('code'); - } - return false; - } else if (event.which === key.ENTER && event.ctrlKey) { - that.execute_selected_cell({terminal:true}); - return false; - } else if (event.which === 77 && event.ctrlKey && that.control_key_active == false) { - that.control_key_active = true; - return false; - } else if (event.which === 88 && that.control_key_active) { - // Cut selected cell = x - that.cut_cell(); - that.control_key_active = false; - return false; - } else if (event.which === 67 && that.control_key_active) { - // Copy selected cell = c - that.copy_cell(); - that.control_key_active = false; - return false; - } else if (event.which === 86 && that.control_key_active) { - // Paste below selected cell = v - that.paste_cell_below(); - that.control_key_active = false; - return false; - } else if (event.which === 68 && that.control_key_active) { - // Delete selected cell = d - that.delete_cell(); - that.control_key_active = false; - return false; - } else if (event.which === 65 && that.control_key_active) { - // Insert code cell above selected = a - that.insert_cell_above('code'); - that.control_key_active = false; - return false; - } else if (event.which === 66 && that.control_key_active) { - // Insert code cell below selected = b - that.insert_cell_below('code'); - that.control_key_active = false; - return false; - } else if (event.which === 89 && that.control_key_active) { - // To code = y - that.to_code(); - that.control_key_active = false; - return false; - } else if (event.which === 77 && that.control_key_active) { - // To markdown = m - that.to_markdown(); - that.control_key_active = false; - return false; - } else if (event.which === 84 && that.control_key_active) { - // To Raw = t - that.to_raw(); - that.control_key_active = false; - return false; - } else if (event.which === 49 && that.control_key_active) { - // To Heading 1 = 1 - that.to_heading(undefined, 1); - that.control_key_active = false; - return false; - } else if (event.which === 50 && that.control_key_active) { - // To Heading 2 = 2 - that.to_heading(undefined, 2); - that.control_key_active = false; - return false; - } else if (event.which === 51 && that.control_key_active) { - // To Heading 3 = 3 - that.to_heading(undefined, 3); - that.control_key_active = false; - return false; - } else if (event.which === 52 && that.control_key_active) { - // To Heading 4 = 4 - that.to_heading(undefined, 4); - that.control_key_active = false; - return false; - } else if (event.which === 53 && that.control_key_active) { - // To Heading 5 = 5 - that.to_heading(undefined, 5); - that.control_key_active = false; - return false; - } else if (event.which === 54 && that.control_key_active) { - // To Heading 6 = 6 - that.to_heading(undefined, 6); - that.control_key_active = false; - return false; - } else if (event.which === 79 && that.control_key_active) { - // Toggle output = o - if (event.shiftKey){ - that.toggle_output_scroll(); - } else { - that.toggle_output(); - } - that.control_key_active = false; - return false; - } else if (event.which === 83 && that.control_key_active) { - // Save notebook = s - that.save_checkpoint(); - that.control_key_active = false; - return false; - } else if (event.which === 74 && that.control_key_active) { - // Move cell down = j - that.move_cell_down(); - that.control_key_active = false; - return false; - } else if (event.which === 75 && that.control_key_active) { - // Move cell up = k - that.move_cell_up(); - that.control_key_active = false; - return false; - } else if (event.which === 80 && that.control_key_active) { - // Select previous = p - that.select_prev(); - that.control_key_active = false; - return false; - } else if (event.which === 78 && that.control_key_active) { - // Select next = n - that.select_next(); - that.control_key_active = false; - return false; - } else if (event.which === 76 && that.control_key_active) { - // Toggle line numbers = l - that.cell_toggle_line_numbers(); - that.control_key_active = false; - return false; - } else if (event.which === 73 && that.control_key_active) { - // Interrupt kernel = i - that.session.interrupt_kernel(); - that.control_key_active = false; - return false; - } else if (event.which === 190 && that.control_key_active) { - // Restart kernel = . # matches qt console - that.restart_kernel(); - that.control_key_active = false; - return false; - } else if (event.which === 72 && that.control_key_active) { - // Show keyboard shortcuts = h - IPython.quick_help.show_keyboard_shortcuts(); - that.control_key_active = false; - return false; - } else if (event.which === 90 && that.control_key_active) { - // Undo last cell delete = z - that.undelete(); - that.control_key_active = false; - return false; - } else if ((event.which === 189 || event.which === 173) && - that.control_key_active) { - // how fun! '-' is 189 in Chrome, but 173 in FF and Opera - // Split cell = - - that.split_cell(); - that.control_key_active = false; - return false; - } else if (that.control_key_active) { - that.control_key_active = false; - return true; - } - return true; - }); - - var collapse_time = function(time){ + var collapse_time = function (time) { var app_height = $('#ipython-main-app').height(); // content height var splitter_height = $('div#pager_splitter').outerHeight(true); var new_height = app_height - splitter_height; that.element.animate({height : new_height + 'px'}, time); - } + }; - this.element.bind('collapse_pager', function (event,extrap) { + this.element.bind('collapse_pager', function (event, extrap) { var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast'; collapse_time(time); }); - var expand_time = function(time) { + var expand_time = function (time) { var app_height = $('#ipython-main-app').height(); // content height var splitter_height = $('div#pager_splitter').outerHeight(true); var pager_height = $('div#pager').outerHeight(true); var new_height = app_height - pager_height - splitter_height; that.element.animate({height : new_height + 'px'}, time); - } + }; this.element.bind('expand_pager', function (event, extrap) { var time = (extrap != undefined) ? ((extrap.duration != undefined ) ? extrap.duration : 'fast') : 'fast'; @@ -650,6 +466,7 @@ var IPython = (function (IPython) { if (this.is_valid_cell_index(index)) { var sindex = this.get_selected_index() if (sindex !== null && index !== sindex) { + this.command_mode(); this.get_cell(sindex).unselect(); }; var cell = this.get_cell(index); @@ -692,6 +509,48 @@ var IPython = (function (IPython) { }; + // Edit/Command mode + + Notebook.prototype.get_edit_index = function () { + var result = null; + this.get_cell_elements().filter(function (index) { + if ($(this).data("cell").mode === 'edit') { + result = index; + }; + }); + return result; + }; + + Notebook.prototype.command_mode = function () { + if (this.mode !== 'command') { + var index = this.get_edit_index(); + var cell = this.get_cell(index); + if (cell) { + cell.command_mode(); + }; + this.mode = 'command'; + IPython.keyboard_manager.command_mode(); + }; + }; + + Notebook.prototype.edit_mode = function () { + if (this.mode !== 'edit') { + var cell = this.get_selected_cell(); + if (cell === null) {return;} // No cell is selected + // We need to set the mode to edit to prevent reentering this method + // when cell.edit_mode() is called below. + this.mode = 'edit'; + IPython.keyboard_manager.edit_mode(); + cell.edit_mode(); + }; + }; + + Notebook.prototype.focus_cell = function () { + var cell = this.get_selected_cell(); + if (cell === null) {return;} // No cell is selected + cell.focus_cell(); + }; + // Cell movement /** @@ -710,6 +569,8 @@ var IPython = (function (IPython) { tomove.detach(); pivot.before(tomove); this.select(i-1); + var cell = this.get_selected_cell(); + cell.focus_cell(); }; this.set_dirty(true); }; @@ -726,13 +587,15 @@ var IPython = (function (IPython) { **/ Notebook.prototype.move_cell_down = function (index) { var i = this.index_or_selected(index); - if ( this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) { + if (this.is_valid_cell_index(i) && this.is_valid_cell_index(i+1)) { var pivot = this.get_cell_element(i+1); var tomove = this.get_cell_element(i); if (pivot !== null && tomove !== null) { tomove.detach(); pivot.after(tomove); this.select(i+1); + var cell = this.get_selected_cell(); + cell.focus_cell(); }; }; this.set_dirty(); @@ -755,9 +618,18 @@ var IPython = (function (IPython) { this.undelete_backup = cell.toJSON(); $('#undelete_cell').removeClass('disabled'); if (this.is_valid_cell_index(i)) { + var old_ncells = this.ncells(); var ce = this.get_cell_element(i); ce.remove(); - if (i === (this.ncells())) { + if (i === 0) { + // Always make sure we have at least one cell. + if (old_ncells === 1) { + this.insert_cell_below('code'); + } + this.select(0); + this.undelete_index = 0; + this.undelete_below = false; + } else if (i === old_ncells-1 && i !== 0) { this.select(i-1); this.undelete_index = i - 1; this.undelete_below = true; @@ -773,6 +645,42 @@ var IPython = (function (IPython) { }; /** + * Restore the most recently deleted cell. + * + * @method undelete + */ + Notebook.prototype.undelete_cell = function() { + if (this.undelete_backup !== null && this.undelete_index !== null) { + var current_index = this.get_selected_index(); + if (this.undelete_index < current_index) { + current_index = current_index + 1; + } + if (this.undelete_index >= this.ncells()) { + this.select(this.ncells() - 1); + } + else { + this.select(this.undelete_index); + } + var cell_data = this.undelete_backup; + var new_cell = null; + if (this.undelete_below) { + new_cell = this.insert_cell_below(cell_data.cell_type); + } else { + new_cell = this.insert_cell_above(cell_data.cell_type); + } + new_cell.fromJSON(cell_data); + if (this.undelete_below) { + this.select(current_index+1); + } else { + this.select(current_index); + } + this.undelete_backup = null; + this.undelete_index = null; + } + $('#undelete_cell').addClass('disabled'); + } + + /** * Insert a cell so that after insertion the cell is at given index. * * Similar to insert_above, but index parameter is mandatory @@ -804,10 +712,14 @@ var IPython = (function (IPython) { cell = new IPython.HeadingCell(); } - if(this._insert_element_at_index(cell.element,index)){ + if(this._insert_element_at_index(cell.element,index)) { cell.render(); - this.select(this.find_cell_index(cell)); $([IPython.events]).trigger('create.Cell', {'cell': cell, 'index': index}); + cell.refresh(); + // We used to select the cell after we refresh it, but there + // are now cases were this method is called where select is + // not appropriate. The selection logic should be handled by the + // caller of the the top level insert_cell methods. this.set_dirty(true); } } @@ -923,6 +835,8 @@ var IPython = (function (IPython) { // to this state, instead of a blank cell target_cell.code_mirror.clearHistory(); source_element.remove(); + this.select(i); + this.edit_mode(); this.set_dirty(true); }; }; @@ -945,13 +859,15 @@ var IPython = (function (IPython) { if (text === source_cell.placeholder) { text = ''; }; - // The edit must come before the set_text. - target_cell.edit(); + // We must show the editor before setting its contents + target_cell.unrender(); target_cell.set_text(text); // make this value the starting point, so that we can only undo // to this state, instead of a blank cell target_cell.code_mirror.clearHistory(); source_element.remove(); + this.select(i); + this.edit_mode(); this.set_dirty(true); }; }; @@ -975,13 +891,15 @@ var IPython = (function (IPython) { if (text === source_cell.placeholder) { text = ''; }; - // The edit must come before the set_text. - target_cell.edit(); + // We must show the editor before setting its contents + target_cell.unrender(); target_cell.set_text(text); // make this value the starting point, so that we can only undo // to this state, instead of a blank cell target_cell.code_mirror.clearHistory(); source_element.remove(); + this.select(i); + this.edit_mode(); this.set_dirty(true); }; }; @@ -1009,16 +927,18 @@ var IPython = (function (IPython) { if (text === source_cell.placeholder) { text = ''; }; - // The edit must come before the set_text. + // We must show the editor before setting its contents target_cell.set_level(level); - target_cell.edit(); + target_cell.unrender(); target_cell.set_text(text); // make this value the starting point, so that we can only undo // to this state, instead of a blank cell target_cell.code_mirror.clearHistory(); source_element.remove(); - this.set_dirty(true); + this.select(i); }; + this.edit_mode(); + this.set_dirty(true); $([IPython.events]).trigger('selected_cell_type_changed.Notebook', {'cell_type':'heading',level:level} ); @@ -1123,40 +1043,6 @@ var IPython = (function (IPython) { }; }; - // Cell undelete - - /** - * Restore the most recently deleted cell. - * - * @method undelete - */ - Notebook.prototype.undelete = function() { - if (this.undelete_backup !== null && this.undelete_index !== null) { - var current_index = this.get_selected_index(); - if (this.undelete_index < current_index) { - current_index = current_index + 1; - } - if (this.undelete_index >= this.ncells()) { - this.select(this.ncells() - 1); - } - else { - this.select(this.undelete_index); - } - var cell_data = this.undelete_backup; - var new_cell = null; - if (this.undelete_below) { - new_cell = this.insert_cell_below(cell_data.cell_type); - } else { - new_cell = this.insert_cell_above(cell_data.cell_type); - } - new_cell.fromJSON(cell_data); - this.select(current_index); - this.undelete_backup = null; - this.undelete_index = null; - } - $('#undelete_cell').addClass('disabled'); - } - // Split/merge /** @@ -1165,24 +1051,25 @@ var IPython = (function (IPython) { * @method split_cell */ Notebook.prototype.split_cell = function () { - // Todo: implement spliting for other cell types. + var mdc = IPython.MarkdownCell; + var rc = IPython.RawCell; var cell = this.get_selected_cell(); if (cell.is_splittable()) { var texta = cell.get_pre_cursor(); var textb = cell.get_post_cursor(); if (cell instanceof IPython.CodeCell) { + // In this case the operations keep the notebook in its existing mode + // so we don't need to do any post-op mode changes. cell.set_text(textb); var new_cell = this.insert_cell_above('code'); new_cell.set_text(texta); - this.select_next(); - } else if (cell instanceof IPython.MarkdownCell) { + } else if ((cell instanceof mdc && !cell.rendered) || (cell instanceof rc)) { + // We know cell is !rendered so we can use set_text. cell.set_text(textb); - cell.render(); - var new_cell = this.insert_cell_above('markdown'); - new_cell.edit(); // editor must be visible to call set_text + var new_cell = this.insert_cell_above(cell.cell_type); + // Unrender the new cell so we can call set_text. + new_cell.unrender(); new_cell.set_text(texta); - new_cell.render(); - this.select_next(); } }; }; @@ -1193,8 +1080,11 @@ var IPython = (function (IPython) { * @method merge_cell_above */ Notebook.prototype.merge_cell_above = function () { + var mdc = IPython.MarkdownCell; + var rc = IPython.RawCell; var index = this.get_selected_index(); var cell = this.get_cell(index); + var render = cell.rendered; if (!cell.is_mergeable()) { return; } @@ -1207,10 +1097,14 @@ var IPython = (function (IPython) { var text = cell.get_text(); if (cell instanceof IPython.CodeCell) { cell.set_text(upper_text+'\n'+text); - } else if (cell instanceof IPython.MarkdownCell) { - cell.edit(); - cell.set_text(upper_text+'\n'+text); - cell.render(); + } else if ((cell instanceof mdc) || (cell instanceof rc)) { + cell.unrender(); // Must unrender before we set_text. + cell.set_text(upper_text+'\n\n'+text); + if (render) { + // The rendered state of the final cell should match + // that of the original selected cell; + cell.render(); + } }; this.delete_cell(index-1); this.select(this.find_cell_index(cell)); @@ -1223,8 +1117,11 @@ var IPython = (function (IPython) { * @method merge_cell_below */ Notebook.prototype.merge_cell_below = function () { + var mdc = IPython.MarkdownCell; + var rc = IPython.RawCell; var index = this.get_selected_index(); var cell = this.get_cell(index); + var render = cell.rendered; if (!cell.is_mergeable()) { return; } @@ -1237,10 +1134,14 @@ var IPython = (function (IPython) { var text = cell.get_text(); if (cell instanceof IPython.CodeCell) { cell.set_text(text+'\n'+lower_text); - } else if (cell instanceof IPython.MarkdownCell) { - cell.edit(); - cell.set_text(text+'\n'+lower_text); - cell.render(); + } else if ((cell instanceof mdc) || (cell instanceof rc)) { + cell.unrender(); // Must unrender before we set_text. + cell.set_text(text+'\n\n'+lower_text); + if (render) { + // The rendered state of the final cell should match + // that of the original selected cell; + cell.render(); + } }; this.delete_cell(index+1); this.select(this.find_cell_index(cell)); @@ -1433,35 +1334,76 @@ var IPython = (function (IPython) { }; /** - * Run the selected cell. + * Execute or render cell outputs and go into command mode. * - * Execute or render cell outputs. + * @method execute_cell + */ + Notebook.prototype.execute_cell = function () { + // mode = shift, ctrl, alt + var cell = this.get_selected_cell(); + var cell_index = this.find_cell_index(cell); + + cell.execute(); + this.command_mode(); + cell.focus_cell(); + this.set_dirty(true); + } + + /** + * Execute or render cell outputs and insert a new cell below. * - * @method execute_selected_cell - * @param {Object} options Customize post-execution behavior + * @method execute_cell_and_insert_below */ - Notebook.prototype.execute_selected_cell = function (options) { - // add_new: should a new cell be added if we are at the end of the nb - // terminal: execute in terminal mode, which stays in the current cell - var default_options = {terminal: false, add_new: true}; - $.extend(default_options, options); - var that = this; - var cell = that.get_selected_cell(); - var cell_index = that.find_cell_index(cell); - if (cell instanceof IPython.CodeCell) { - cell.execute(); + Notebook.prototype.execute_cell_and_insert_below = function () { + var cell = this.get_selected_cell(); + var cell_index = this.find_cell_index(cell); + + cell.execute(); + + // If we are at the end always insert a new cell and return + if (cell_index === (this.ncells()-1)) { + this.insert_cell_below('code'); + this.select(cell_index+1); + this.edit_mode(); + this.scroll_to_bottom(); + this.set_dirty(true); + return; } - if (default_options.terminal) { - cell.select_all(); - } else { - if ((cell_index === (that.ncells()-1)) && default_options.add_new) { - that.insert_cell_below('code'); - // If we are adding a new cell at the end, scroll down to show it. - that.scroll_to_bottom(); - } else { - that.select(cell_index+1); - }; - }; + + // Only insert a new cell, if we ended up in an already populated cell + var next_text = this.get_cell(cell_index+1).get_text(); + if (/\S/.test(next_text) === true) { + this.insert_cell_below('code'); + } + this.select(cell_index+1); + this.edit_mode(); + this.set_dirty(true); + }; + + /** + * Execute or render cell outputs and select the next cell. + * + * @method execute_cell_and_select_below + */ + Notebook.prototype.execute_cell_and_select_below = function () { + + var cell = this.get_selected_cell(); + var cell_index = this.find_cell_index(cell); + + cell.execute(); + + // If we are at the end always insert a new cell and return + if (cell_index === (this.ncells()-1)) { + this.insert_cell_below('code'); + this.select(cell_index+1); + this.edit_mode(); + this.scroll_to_bottom(); + this.set_dirty(true); + return; + } + + this.select(cell_index+1); + this.get_cell(cell_index+1).focus_cell(); this.set_dirty(true); }; @@ -1504,7 +1446,7 @@ var IPython = (function (IPython) { Notebook.prototype.execute_cell_range = function (start, end) { for (var i=start; i