diff --git a/IPython/html/static/base/js/keyboard.js b/IPython/html/static/base/js/keyboard.js index c8aa919..474b240 100644 --- a/IPython/html/static/base/js/keyboard.js +++ b/IPython/html/static/base/js/keyboard.js @@ -12,18 +12,22 @@ define([ 'base/js/namespace', 'jquery', 'base/js/utils', -], function(IPython, $, utils) { + 'underscore', +], function(IPython, $, utils, _) { "use strict"; - // Setup global keycodes and inverse keycodes. + /** + * 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. + **/ - // 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) + // These apply to Firefox, (Webkit and IE) + // This does work **only** on US keyboard. 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, @@ -84,13 +88,32 @@ define([ }; var normalize_shortcut = function (shortcut) { - // Put a shortcut into normalized form: - // 1. Make lowercase - // 2. Replace cmd by meta - // 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift - // 4. Normalize keys + /** + * @function _normalize_shortcut + * @private + * return a dict containing the normalized shortcut and the number of time it should be pressed: + * + * Put a shortcut into normalized form: + * 1. Make lowercase + * 2. Replace cmd by meta + * 3. Sort '-' separated modifiers into the order alt-ctrl-meta-shift + * 4. Normalize keys + **/ + if (platform === 'MacOS') { + shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'cmd-'); + } else { + shortcut = shortcut.toLowerCase().replace('cmdtrl-', 'ctrl-'); + } + shortcut = shortcut.toLowerCase().replace('cmd', 'meta'); shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key + shortcut = shortcut.replace(/,$/, 'comma'); // catch shortcuts using '-' key + if(shortcut.indexOf(',') !== -1){ + var sht = shortcut.split(','); + sht = _.map(sht, normalize_shortcut); + return shortcut; + } + shortcut = shortcut.replace(/comma/g, ','); // catch shortcuts using '-' key var values = shortcut.split("-"); if (values.length === 1) { return normalize_key(values[0]); @@ -103,7 +126,9 @@ define([ }; var shortcut_to_event = function (shortcut, type) { - // Convert a shortcut (shift-r) to a jQuery Event object + /** + * Convert a shortcut (shift-r) to a jQuery Event object + **/ type = type || 'keydown'; shortcut = normalize_shortcut(shortcut); shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key @@ -118,8 +143,21 @@ define([ return $.Event(type, opts); }; + var only_modifier_event = function(event){ + /** + * Return `true` if the event only contains modifiers keys. + * false otherwise + **/ + var key = inv_keycodes[event.which]; + return ((event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) && + (key === 'alt'|| key === 'ctrl'|| key === 'meta'|| key === 'shift')); + + }; + var event_to_shortcut = function (event) { - // Convert a jQuery Event object to a shortcut (shift-r) + /** + * Convert a jQuery Event object to a normalized shortcut string (shift-r) + **/ var shortcut = ''; var key = inv_keycodes[event.which]; if (event.altKey && key !== 'alt') {shortcut += 'alt-';} @@ -132,7 +170,7 @@ define([ // Shortcut manager class - var ShortcutManager = function (delay, events) { + var ShortcutManager = function (delay, events, actions, env) { /** * A class to deal with keyboard event and shortcut * @@ -140,33 +178,78 @@ define([ * @constructor */ this._shortcuts = {}; - this._counts = {}; - this._timers = {}; this.delay = delay || 800; // delay in milliseconds this.events = events; + this.actions = actions; + this.actions.extend_env(env); + this._queue = []; + this._cleartimeout = null; + Object.seal(this); + }; + + ShortcutManager.prototype.clearsoon = function(){ + /** + * Clear the pending shortcut soon, and cancel previous clearing + * that might be registered. + **/ + var that = this; + clearTimeout(this._cleartimeout); + this._cleartimeout = setTimeout(function(){that.clearqueue();}, this.delay); + }; + + + ShortcutManager.prototype.clearqueue = function(){ + /** + * clear the pending shortcut sequence now. + **/ + this._queue = []; + clearTimeout(this._cleartimeout); + }; + + + var flatten_shorttree = function(tree){ + /** + * Flatten a tree of shortcut sequences. + * use full to iterate over all the key/values of available shortcuts. + **/ + var dct = {}; + for(var key in tree){ + var value = tree[key]; + if(typeof(value) === 'string'){ + dct[key] = value; + } else { + var ftree=flatten_shorttree(value); + for(var subkey in ftree){ + dct[key+','+subkey] = ftree[subkey]; + } + } + } + return dct; }; 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; + var ftree = flatten_shorttree(this._shortcuts); + for (var shortcut in ftree) { + var action = this.actions.get(ftree[shortcut]); + var help_string = action.help||'== no help =='; + var help_index = action.help_index; if (help_string) { - if (platform === 'MacOS') { - shortcut = shortcut.replace('meta', 'cmd'); - } + var shortstring = (action.shortstring||shortcut); help.push({ - shortcut: shortcut, + shortcut: shortstring, help: help_string, help_index: help_index} ); } } help.sort(function (a, b) { - if (a.help_index > b.help_index) + if (a.help_index > b.help_index){ return 1; - if (a.help_index < b.help_index) + } + if (a.help_index < b.help_index){ return -1; + } return 0; }); return help; @@ -176,19 +259,105 @@ define([ this._shortcuts = {}; }; - ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) { - if (typeof(data) === 'function') { - data = {help: '', help_index: '', handler: data}; + ShortcutManager.prototype.get_shortcut = function (shortcut){ + /** + * return a node of the shortcut tree which an action name (string) if leaf, + * and an object with `object.subtree===true` + **/ + if(typeof(shortcut) === 'string'){ + shortcut = shortcut.split(','); + } + + return this._get_leaf(shortcut, this._shortcuts); + }; + + + ShortcutManager.prototype._get_leaf = function(shortcut_array, tree){ + /** + * @private + * find a leaf/node in a subtree of the keyboard shortcut + * + **/ + if(shortcut_array.length === 1){ + return tree[shortcut_array[0]]; + } else if( typeof(tree[shortcut_array[0]]) !== 'string'){ + return this._get_leaf(shortcut_array.slice(1), tree[shortcut_array[0]]); + } + return null; + }; + + ShortcutManager.prototype.set_shortcut = function( shortcut, action_name){ + if( typeof(action_name) !== 'string'){ throw('action is not a string', action_name);} + if( typeof(shortcut) === 'string'){ + shortcut = shortcut.split(','); + } + return this._set_leaf(shortcut, action_name, this._shortcuts); + }; + + ShortcutManager.prototype._is_leaf = function(shortcut_array, tree){ + if(shortcut_array.length === 1){ + return(typeof(tree[shortcut_array[0]]) === 'string'); + } else { + var subtree = tree[shortcut_array[0]]; + return this._is_leaf(shortcut_array.slice(1), subtree ); + } + }; + + ShortcutManager.prototype._remove_leaf = function(shortcut_array, tree, allow_node){ + if(shortcut_array.length === 1){ + var current_node = tree[shortcut_array[0]]; + if(typeof(current_node) === 'string'){ + delete tree[shortcut_array[0]]; + } else { + throw('try to delete non-leaf'); + } + } else { + this._remove_leaf(shortcut_array.slice(1), tree[shortcut_array[0]], allow_node); + if(_.keys(tree[shortcut_array[0]]).length === 0){ + delete tree[shortcut_array[0]]; + } + } + }; + + ShortcutManager.prototype._set_leaf = function(shortcut_array, action_name, tree){ + var current_node = tree[shortcut_array[0]]; + if(shortcut_array.length === 1){ + if(current_node !== undefined && typeof(current_node) !== 'string'){ + console.warn('[warning], you are overriting a long shortcut with a shorter one'); + } + tree[shortcut_array[0]] = action_name; + return true; + } else { + if(typeof(current_node) === 'string'){ + console.warn('you are trying to set a shortcut that will be shadowed'+ + 'by a more specific one. Aborting for :', action_name, 'the follwing '+ + 'will take precedence', current_node); + return false; + } else { + tree[shortcut_array[0]] = tree[shortcut_array[0]]||{}; + } + this._set_leaf(shortcut_array.slice(1), action_name, tree[shortcut_array[0]]); + return true; } - data.help_index = data.help_index || ''; - data.help = data.help || ''; - data.count = data.count || 1; - if (data.help_index === '') { - data.help_index = 'zz'; + }; + + ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) { + /** + * Add a action to be handled by shortcut manager. + * + * - `shortcut` should be a `Shortcut Sequence` of the for `Ctrl-Alt-C,Meta-X`... + * - `data` could be an `action name`, an `action` or a `function`. + * if a `function` is passed it will be converted to an anonymous `action`. + * + **/ + var action_name = this.actions.get_name(data); + if (! action_name){ + throw('does nto know how to deal with ', data); } + shortcut = normalize_shortcut(shortcut); - this._counts[shortcut] = 0; - this._shortcuts[shortcut] = data; + this.set_shortcut(shortcut, action_name); + if (!suppress_help_update) { // update the keyboard shortcuts notebook help this.events.trigger('rebuild.QuickHelp'); @@ -196,6 +365,11 @@ define([ }; ShortcutManager.prototype.add_shortcuts = function (data) { + /** + * Convenient methods to call `add_shortcut(key, value)` on several items + * + * data : Dict of the form {key:value, ...} + **/ for (var shortcut in data) { this.add_shortcut(shortcut, data[shortcut], true); } @@ -204,44 +378,22 @@ define([ }; ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) { + /** + * Remove the binding of shortcut `sortcut` with its action. + * throw an error if trying to remove a non-exiting shortcut + **/ shortcut = normalize_shortcut(shortcut); - delete this._counts[shortcut]; - delete this._shortcuts[shortcut]; + if( typeof(shortcut) === 'string'){ + shortcut = shortcut.split(','); + } + this._remove_leaf(shortcut, this._shortcuts); if (!suppress_help_update) { // update the keyboard shortcuts notebook help this.events.trigger('rebuild.QuickHelp'); } }; - ShortcutManager.prototype.count_handler = function (shortcut, event, data) { - /** - * Seem to allow to call an handler only after several key press. - * like, I suppose `dd` that delete the current cell only after - * `d` has been pressed twice.. - * @method count_handler - * @return {Boolean} `true|false`, whether or not the event has been handled. - * @param shortcut {shortcut} - * @param event {event} - * @param data {data} - */ - var that = this; - var c = this._counts; - var t = this._timers; - var timer = null; - if (c[shortcut] === data.count-1) { - c[shortcut] = 0; - timer = t[shortcut]; - if (timer) {clearTimeout(timer); delete t[shortcut];} - return data.handler(event); - } else { - c[shortcut] = c[shortcut] + 1; - timer = setTimeout(function () { - c[shortcut] = 0; - }, that.delay); - t[shortcut] = timer; - } - return false; - }; + ShortcutManager.prototype.call_handler = function (event) { /** @@ -249,26 +401,40 @@ define([ * @method call_handler * @return {Boolean} `true|false`, `false` if no handler was found, otherwise the value return by the handler. * @param event {event} - */ + * + * given an event, call the corresponding shortcut. + * return false is event wan handled, true otherwise + * in any case returning false stop event propagation + **/ + + + this.clearsoon(); + if(only_modifier_event(event)){ + return true; + } var shortcut = event_to_shortcut(event); - var data = this._shortcuts[shortcut]; - if (data) { - var handler = data.handler; - if (handler) { - if (data.count === 1) { - return handler(event); - } else if (data.count > 1) { - return this.count_handler(shortcut, event, data); - } - } + this._queue.push(shortcut); + var action_name = this.get_shortcut(this._queue); + + if (typeof(action_name) === 'undefined'|| action_name === null){ + this.clearqueue(); + return true; + } + + if (this.actions.exists(action_name)) { + event.preventDefault(); + this.clearqueue(); + return this.actions.call(action_name, event); } - return true; + + return false; }; + ShortcutManager.prototype.handles = function (event) { var shortcut = event_to_shortcut(event); - var data = this._shortcuts[shortcut]; - return !( data === undefined || data.handler === undefined ); + var action_name = this.get_shortcut(this._queue.concat(shortcut)); + return (typeof(action_name) !== 'undefined'); }; var keyboard = { @@ -278,7 +444,7 @@ define([ normalize_key : normalize_key, normalize_shortcut : normalize_shortcut, shortcut_to_event : shortcut_to_event, - event_to_shortcut : event_to_shortcut + event_to_shortcut : event_to_shortcut, }; // For backwards compatibility. diff --git a/IPython/html/static/notebook/js/actions.js b/IPython/html/static/notebook/js/actions.js new file mode 100644 index 0000000..f91091d --- /dev/null +++ b/IPython/html/static/notebook/js/actions.js @@ -0,0 +1,503 @@ +// Copyright (c) IPython Development Team. +// Distributed under the terms of the Modified BSD License. + +define(['require' +], function(require) { + "use strict"; + + var ActionHandler = function (env) { + this.env = env || {}; + Object.seal(this); + }; + + /** + * A bunch of predefined `Simple Actions` used by IPython. + * `Simple Actions` have the following keys: + * help (optional): a short string the describe the action. + * will be used in various context, like as menu name, tool tips on buttons, + * and short description in help menu. + * help_index (optional): a string used to sort action in help menu. + * icon (optional): a short string that represent the icon that have to be used with this + * action. this should mainly correspond to a Font_awesome class. + * handler : a function which is called when the action is activated. It will receive at first parameter + * a dictionary containing various handle to element of the notebook. + * + * action need to be registered with a **name** that can be use to refer to this action. + * + * + * if `help` is not provided it will be derived by replacing any dash by space + * in the **name** of the action. It is advised to provide a prefix to action name to + * avoid conflict the prefix should be all lowercase and end with a dot `.` + * in the absence of a prefix the behavior of the action is undefined. + * + * All action provided by IPython are prefixed with `ipython.`. + * + * One can register extra actions or replace an existing action with another one is possible + * but is considered undefined behavior. + * + **/ + var _action = { + 'run-select-next': { + icon: 'fa-play', + help : 'run cell, select below', + help_index : 'ba', + handler : function (env) { + env.notebook.execute_cell_and_select_below(); + } + }, + 'execute-in-place':{ + help : 'run cell', + help_index : 'bb', + handler : function (env) { + env.notebook.execute_cell(); + } + }, + 'execute-and-insert-after':{ + help : 'run cell, insert below', + help_index : 'bc', + handler : function (env) { + env.notebook.execute_cell_and_insert_below(); + } + }, + 'go-to-command-mode': { + help : 'command mode', + help_index : 'aa', + handler : function (env) { + env.notebook.command_mode(); + } + }, + 'split-cell-at-cursor': { + help : 'split cell', + help_index : 'ea', + handler : function (env) { + env.notebook.split_cell(); + } + }, + 'enter-edit-mode' : { + help_index : 'aa', + handler : function (env) { + env.notebook.edit_mode(); + } + }, + 'select-previous-cell' : { + help_index : 'da', + handler : function (env) { + var index = env.notebook.get_selected_index(); + if (index !== 0 && index !== null) { + env.notebook.select_prev(); + env.notebook.focus_cell(); + } + } + }, + 'select-next-cell' : { + help_index : 'db', + handler : function (env) { + var index = env.notebook.get_selected_index(); + if (index !== (env.notebook.ncells()-1) && index !== null) { + env.notebook.select_next(); + env.notebook.focus_cell(); + } + } + }, + 'cut-selected-cell' : { + icon: 'fa-cut', + help_index : 'ee', + handler : function (env) { + env.notebook.cut_cell(); + } + }, + 'copy-selected-cell' : { + icon: 'fa-copy', + help_index : 'ef', + handler : function (env) { + env.notebook.copy_cell(); + } + }, + 'paste-cell-before' : { + help_index : 'eg', + handler : function (env) { + env.notebook.paste_cell_above(); + } + }, + 'paste-cell-after' : { + icon: 'fa-paste', + help_index : 'eh', + handler : function (env) { + env.notebook.paste_cell_below(); + } + }, + 'insert-cell-before' : { + help_index : 'ec', + handler : function (env) { + env.notebook.insert_cell_above(); + env.notebook.select_prev(); + env.notebook.focus_cell(); + } + }, + 'insert-cell-after' : { + icon : 'fa-plus', + help_index : 'ed', + handler : function (env) { + env.notebook.insert_cell_below(); + env.notebook.select_next(); + env.notebook.focus_cell(); + } + }, + 'change-selected-cell-to-code-cell' : { + help : 'to code', + help_index : 'ca', + handler : function (env) { + env.notebook.to_code(); + } + }, + 'change-selected-cell-to-markdown-cell' : { + help : 'to markdown', + help_index : 'cb', + handler : function (env) { + env.notebook.to_markdown(); + } + }, + 'change-selected-cell-to-raw-cell' : { + help : 'to raw', + help_index : 'cc', + handler : function (env) { + env.notebook.to_raw(); + } + }, + 'change-selected-cell-to-heading-1' : { + help : 'to heading 1', + help_index : 'cd', + handler : function (env) { + env.notebook.to_heading(undefined, 1); + } + }, + 'change-selected-cell-to-heading-2' : { + help : 'to heading 2', + help_index : 'ce', + handler : function (env) { + env.notebook.to_heading(undefined, 2); + } + }, + 'change-selected-cell-to-heading-3' : { + help : 'to heading 3', + help_index : 'cf', + handler : function (env) { + env.notebook.to_heading(undefined, 3); + } + }, + 'change-selected-cell-to-heading-4' : { + help : 'to heading 4', + help_index : 'cg', + handler : function (env) { + env.notebook.to_heading(undefined, 4); + } + }, + 'change-selected-cell-to-heading-5' : { + help : 'to heading 5', + help_index : 'ch', + handler : function (env) { + env.notebook.to_heading(undefined, 5); + } + }, + 'change-selected-cell-to-heading-6' : { + help : 'to heading 6', + help_index : 'ci', + handler : function (env) { + env.notebook.to_heading(undefined, 6); + } + }, + 'toggle-output-visibility-selected-cell' : { + help : 'toggle output', + help_index : 'gb', + handler : function (env) { + env.notebook.toggle_output(); + } + }, + 'toggle-output-scrolling-selected-cell' : { + help : 'toggle output scrolling', + help_index : 'gc', + handler : function (env) { + env.notebook.toggle_output_scroll(); + } + }, + 'move-selected-cell-down' : { + icon: 'fa-arrow-down', + help_index : 'eb', + handler : function (env) { + env.notebook.move_cell_down(); + } + }, + 'move-selected-cell-up' : { + icon: 'fa-arrow-up', + help_index : 'ea', + handler : function (env) { + env.notebook.move_cell_up(); + } + }, + 'toggle-line-number-selected-cell' : { + help : 'toggle line numbers', + help_index : 'ga', + handler : function (env) { + env.notebook.cell_toggle_line_numbers(); + } + }, + 'show-keyboard-shortcut-help-dialog' : { + help_index : 'ge', + handler : function (env) { + env.quick_help.show_keyboard_shortcuts(); + } + }, + 'delete-cell': { + help_index : 'ej', + handler : function (env) { + env.notebook.delete_cell(); + } + }, + 'interrupt-kernel':{ + icon: 'fa-stop', + help_index : 'ha', + handler : function (env) { + env.notebook.kernel.interrupt(); + } + }, + 'restart-kernel':{ + icon: 'fa-repeat', + help_index : 'hb', + handler : function (env) { + env.notebook.restart_kernel(); + } + }, + 'undo-last-cell-deletion' : { + help_index : 'ei', + handler : function (env) { + env.notebook.undelete_cell(); + } + }, + 'merge-selected-cell-with-cell-after' : { + help : 'merge cell below', + help_index : 'ek', + handler : function (env) { + env.notebook.merge_cell_below(); + } + }, + 'close-pager' : { + help_index : 'gd', + handler : function (env) { + env.pager.collapse(); + } + } + + }; + + /** + * A bunch of `Advance actions` for IPython. + * Cf `Simple Action` plus the following properties. + * + * handler: first argument of the handler is the event that triggerd the action + * (typically keypress). The handler is responsible for any modification of the + * event and event propagation. + * Is also responsible for returning false if the event have to be further ignored, + * true, to tell keyboard manager that it ignored the event. + * + * the second parameter of the handler is the environemnt passed to Simple Actions + * + **/ + var custom_ignore = { + 'ignore':{ + handler : function () { + return true; + } + }, + 'move-cursor-up-or-previous-cell':{ + handler : function (env, event) { + var index = env.notebook.get_selected_index(); + var cell = env.notebook.get_cell(index); + var cm = env.notebook.get_selected_cell().code_mirror; + var cur = cm.getCursor(); + if (cell && cell.at_top() && index !== 0 && cur.ch === 0) { + if(event){ + event.preventDefault(); + } + env.notebook.command_mode(); + env.notebook.select_prev(); + env.notebook.edit_mode(); + cm = env.notebook.get_selected_cell().code_mirror; + cm.setCursor(cm.lastLine(), 0); + } + return false; + } + }, + 'move-cursor-down-or-next-cell':{ + handler : function (env, event) { + var index = env.notebook.get_selected_index(); + var cell = env.notebook.get_cell(index); + if (cell.at_bottom() && index !== (env.notebook.ncells()-1)) { + if(event){ + event.preventDefault(); + } + env.notebook.command_mode(); + env.notebook.select_next(); + env.notebook.edit_mode(); + var cm = env.notebook.get_selected_cell().code_mirror; + cm.setCursor(0, 0); + } + return false; + } + }, + 'scroll-down': { + handler: function(env, event) { + if(event){ + event.preventDefault(); + } + return env.notebook.scroll_manager.scroll(1); + }, + }, + 'scroll-up': { + handler: function(env, event) { + if(event){ + event.preventDefault(); + } + return env.notebook.scroll_manager.scroll(-1); + }, + }, + 'save-notebook':{ + help: "Save and Checkpoint", + help_index : 'fb', + icon: 'fa-save', + handler : function (env, event) { + env.notebook.save_checkpoint(); + if(event){ + event.preventDefault(); + } + return false; + } + }, + }; + + // private stuff that prepend `.ipython` to actions names + // and uniformize/fill in missing pieces in of an action. + var _prepare_handler = function(registry, subkey, source){ + registry['ipython.'+subkey] = {}; + registry['ipython.'+subkey].help = source[subkey].help||subkey.replace(/-/g,' '); + registry['ipython.'+subkey].help_index = source[subkey].help_index; + registry['ipython.'+subkey].icon = source[subkey].icon; + return source[subkey].handler; + }; + + // Will actually generate/register all the IPython actions + var fun = function(){ + var final_actions = {}; + for(var k in _action){ + // Js closure are function level not block level need to wrap in a IIFE + // and append ipython to event name these things do intercept event so are wrapped + // in a function that return false. + var handler = _prepare_handler(final_actions, k, _action); + (function(key, handler){ + final_actions['ipython.'+key].handler = function(env, event){ + handler(env); + if(event){ + event.preventDefault(); + } + return false; + }; + })(k, handler); + } + + for(var k in custom_ignore){ + // Js closure are function level not block level need to wrap in a IIFE + // same as above, but decide for themselves wether or not they intercept events. + var handler = _prepare_handler(final_actions, k, custom_ignore); + (function(key, handler){ + final_actions['ipython.'+key].handler = function(env, event){ + return handler(env, event); + }; + })(k, handler); + } + + return final_actions; + }; + ActionHandler.prototype._actions = fun(); + + + /** + * extend the environment variable that will be pass to handlers + **/ + ActionHandler.prototype.extend_env = function(env){ + for(var k in env){ + this.env[k] = env[k]; + } + }; + + ActionHandler.prototype.register = function(action, name, prefix){ + /** + * Register an `action` with an optional name and prefix. + * + * if name and prefix are not given they will be determined automatically. + * if action if just a `function` it will be wrapped in an anonymous action. + * + * @return the full name to access this action . + **/ + action = this.normalise(action); + if( !name ){ + name = 'autogenerated-'+String(action.handler); + } + prefix = prefix || 'auto'; + var full_name = prefix+'.'+name; + this._actions[full_name] = action; + return full_name; + + }; + + + ActionHandler.prototype.normalise = function(data){ + /** + * given an `action` or `function`, return a normalised `action` + * by setting all known attributes and removing unknown attributes; + **/ + if(typeof(data) === 'function'){ + data = {handler:data}; + } + if(typeof(data.handler) !== 'function'){ + throw('unknown datatype, cannot register'); + } + var _data = data; + data = {}; + data.handler = _data.handler; + data.help = data.help || ''; + data.icon = data.icon || ''; + data.help_index = data.help_index || ''; + return data; + }; + + ActionHandler.prototype.get_name = function(name_or_data){ + /** + * given an `action` or `name` of a action, return the name attached to this action. + * if given the name of and corresponding actions does not exist in registry, return `null`. + **/ + + if(typeof(name_or_data) === 'string'){ + if(this.exists(name_or_data)){ + return name_or_data; + } else { + return null; + } + } else { + return this.register(name_or_data); + } + }; + + ActionHandler.prototype.get = function(name){ + return this._actions[name]; + }; + + ActionHandler.prototype.call = function(name, event, env){ + return this._actions[name].handler(env|| this.env, event); + }; + + ActionHandler.prototype.exists = function(name){ + return (typeof(this._actions[name]) !== 'undefined'); + }; + + return {init:ActionHandler}; + +}); diff --git a/IPython/html/static/notebook/js/keyboardmanager.js b/IPython/html/static/notebook/js/keyboardmanager.js index 3127ddc..757007d 100644 --- a/IPython/html/static/notebook/js/keyboardmanager.js +++ b/IPython/html/static/notebook/js/keyboardmanager.js @@ -16,9 +16,6 @@ define([ ], function(IPython, $, utils, keyboard) { "use strict"; - var browser = utils.browser[0]; - var platform = utils.platform; - // Main keyboard manager for the notebook var keycodes = keyboard.keycodes; @@ -37,470 +34,101 @@ define([ this.pager = options.pager; this.quick_help = undefined; this.notebook = undefined; + this.last_mode = undefined; this.bind_events(); - this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events); + this.env = {pager:this.pager}; + this.actions = options.actions; + this.command_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env ); this.command_shortcuts.add_shortcuts(this.get_default_common_shortcuts()); this.command_shortcuts.add_shortcuts(this.get_default_command_shortcuts()); - this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events); + this.edit_shortcuts = new keyboard.ShortcutManager(undefined, options.events, this.actions, this.env); this.edit_shortcuts.add_shortcuts(this.get_default_common_shortcuts()); this.edit_shortcuts.add_shortcuts(this.get_default_edit_shortcuts()); + Object.seal(this); }; + + + /** * Return a dict of common shortcut * @method get_default_common_shortcuts * * @example Example of returned shortcut * ``` - * 'shortcut-key': // a string representing the shortcut as dash separated value. - * // e.g. 'shift' , 'shift-enter', 'cmd-t' - * { - * help: String // user facing help string - * help_index: String // string used internally to order the shortcut on the quickhelp - * handler: function(event){return true|false} // function that takes an even as first and only parameter - * // and return a boolean indicating whether or not the event should been handled further. - * } + * 'shortcut-key': 'action-name' + * // a string representing the shortcut as dash separated value. + * // e.g. 'shift' , 'shift-enter', 'cmd-t' *``` */ KeyboardManager.prototype.get_default_common_shortcuts = function() { - var that = this; - var shortcuts = { - 'shift' : { - help : '', - help_index : '', - handler : function (event) { - // ignore shift keydown - return true; - } - }, - 'shift-enter' : { - help : 'run cell, select below', - help_index : 'ba', - handler : function (event) { - that.notebook.execute_cell_and_select_below(); - return false; - } - }, - 'ctrl-enter' : { - help : 'run cell', - help_index : 'bb', - handler : function (event) { - that.notebook.execute_cell(); - return false; - } - }, - 'alt-enter' : { - help : 'run cell, insert below', - help_index : 'bc', - handler : function (event) { - that.notebook.execute_cell_and_insert_below(); - return false; - } - } + return { + 'shift' : 'ipython.ignore', + 'shift-enter' : 'ipython.run-select-next', + 'ctrl-enter' : 'ipython.execute-in-place', + 'alt-enter' : 'ipython.execute-and-insert-after', + // cmd on mac, ctrl otherwise + 'cmdtrl-s' : 'ipython.save-notebook', }; - - if (platform === 'MacOS') { - shortcuts['cmd-s'] = - { - help : 'save notebook', - help_index : 'fb', - handler : function (event) { - that.notebook.save_checkpoint(); - event.preventDefault(); - return false; - } - }; - } else { - shortcuts['ctrl-s'] = - { - help : 'save notebook', - help_index : 'fb', - handler : function (event) { - that.notebook.save_checkpoint(); - event.preventDefault(); - return false; - } - }; - } - return shortcuts; }; KeyboardManager.prototype.get_default_edit_shortcuts = function() { - var that = this; return { - 'esc' : { - help : 'command mode', - help_index : 'aa', - handler : function (event) { - that.notebook.command_mode(); - return false; - } - }, - 'ctrl-m' : { - help : 'command mode', - help_index : 'ab', - handler : function (event) { - that.notebook.command_mode(); - return false; - } - }, - 'up' : { - help : '', - help_index : '', - handler : function (event) { - var index = that.notebook.get_selected_index(); - var cell = that.notebook.get_cell(index); - var cm = that.notebook.get_selected_cell().code_mirror; - var cur = cm.getCursor() - if (cell && cell.at_top() && index !== 0 && cur.ch === 0) { - event.preventDefault(); - that.notebook.command_mode(); - that.notebook.select_prev(); - that.notebook.edit_mode(); - var cm = that.notebook.get_selected_cell().code_mirror; - cm.setCursor(cm.lastLine(), 0); - } - return false; - } - }, - 'down' : { - help : '', - help_index : '', - handler : function (event) { - var index = that.notebook.get_selected_index(); - var cell = that.notebook.get_cell(index); - if (cell.at_bottom() && index !== (that.notebook.ncells()-1)) { - event.preventDefault(); - that.notebook.command_mode(); - that.notebook.select_next(); - that.notebook.edit_mode(); - var cm = that.notebook.get_selected_cell().code_mirror; - cm.setCursor(0, 0); - return false; - } - return false; - } - }, - 'ctrl-shift--' : { - help : 'split cell', - help_index : 'ea', - handler : function (event) { - that.notebook.split_cell(); - return false; - } - }, - 'ctrl-shift-subtract' : { - help : '', - help_index : 'eb', - handler : function (event) { - that.notebook.split_cell(); - return false; - } - }, + 'esc' : 'ipython.go-to-command-mode', + 'ctrl-m' : 'ipython.go-to-command-mode', + 'up' : 'ipython.move-cursor-up-or-previous-cell', + 'down' : 'ipython.move-cursor-down-or-next-cell', + 'ctrl-shift--' : 'ipython.split-cell-at-cursor', + 'ctrl-shift-subtract' : 'ipython.split-cell-at-cursor' }; }; KeyboardManager.prototype.get_default_command_shortcuts = function() { - var that = this; return { - 'space': { - help: "Scroll down", - handler: function(event) { - return that.notebook.scroll_manager.scroll(1); - }, - }, - 'shift-space': { - help: "Scroll up", - handler: function(event) { - return that.notebook.scroll_manager.scroll(-1); - }, - }, - 'enter' : { - help : 'edit mode', - help_index : 'aa', - handler : function (event) { - that.notebook.edit_mode(); - return false; - } - }, - 'up' : { - help : 'select previous cell', - help_index : 'da', - handler : function (event) { - var index = that.notebook.get_selected_index(); - if (index !== 0 && index !== null) { - that.notebook.select_prev(); - that.notebook.focus_cell(); - } - return false; - } - }, - 'down' : { - help : 'select next cell', - help_index : 'db', - handler : function (event) { - var index = that.notebook.get_selected_index(); - if (index !== (that.notebook.ncells()-1) && index !== null) { - that.notebook.select_next(); - that.notebook.focus_cell(); - } - return false; - } - }, - 'k' : { - help : 'select previous cell', - help_index : 'dc', - handler : function (event) { - var index = that.notebook.get_selected_index(); - if (index !== 0 && index !== null) { - that.notebook.select_prev(); - that.notebook.focus_cell(); - } - return false; - } - }, - 'j' : { - help : 'select next cell', - help_index : 'dd', - handler : function (event) { - var index = that.notebook.get_selected_index(); - if (index !== (that.notebook.ncells()-1) && index !== null) { - that.notebook.select_next(); - that.notebook.focus_cell(); - } - return false; - } - }, - 'x' : { - help : 'cut cell', - help_index : 'ee', - handler : function (event) { - that.notebook.cut_cell(); - return false; - } - }, - 'c' : { - help : 'copy cell', - help_index : 'ef', - handler : function (event) { - that.notebook.copy_cell(); - return false; - } - }, - 'shift-v' : { - help : 'paste cell above', - help_index : 'eg', - handler : function (event) { - that.notebook.paste_cell_above(); - return false; - } - }, - 'v' : { - help : 'paste cell below', - help_index : 'eh', - handler : function (event) { - that.notebook.paste_cell_below(); - return false; - } - }, - 'd' : { - help : 'delete cell (press twice)', - help_index : 'ej', - count: 2, - handler : function (event) { - that.notebook.delete_cell(); - return false; - } - }, - 'a' : { - help : 'insert cell above', - help_index : 'ec', - handler : function (event) { - that.notebook.insert_cell_above(); - that.notebook.select_prev(); - that.notebook.focus_cell(); - return false; - } - }, - 'b' : { - help : 'insert cell below', - help_index : 'ed', - handler : function (event) { - that.notebook.insert_cell_below(); - that.notebook.select_next(); - that.notebook.focus_cell(); - return false; - } - }, - 'y' : { - help : 'to code', - help_index : 'ca', - handler : function (event) { - that.notebook.to_code(); - return false; - } - }, - 'm' : { - help : 'to markdown', - help_index : 'cb', - handler : function (event) { - that.notebook.to_markdown(); - return false; - } - }, - 'r' : { - help : 'to raw', - help_index : 'cc', - handler : function (event) { - that.notebook.to_raw(); - return false; - } - }, - '1' : { - help : 'to heading 1', - help_index : 'cd', - handler : function (event) { - that.notebook.to_heading(undefined, 1); - return false; - } - }, - '2' : { - help : 'to heading 2', - help_index : 'ce', - handler : function (event) { - that.notebook.to_heading(undefined, 2); - return false; - } - }, - '3' : { - help : 'to heading 3', - help_index : 'cf', - handler : function (event) { - that.notebook.to_heading(undefined, 3); - return false; - } - }, - '4' : { - help : 'to heading 4', - help_index : 'cg', - handler : function (event) { - that.notebook.to_heading(undefined, 4); - return false; - } - }, - '5' : { - help : 'to heading 5', - help_index : 'ch', - handler : function (event) { - that.notebook.to_heading(undefined, 5); - return false; - } - }, - '6' : { - help : 'to heading 6', - help_index : 'ci', - handler : function (event) { - that.notebook.to_heading(undefined, 6); - return false; - } - }, - 'o' : { - help : 'toggle output', - help_index : 'gb', - handler : function (event) { - that.notebook.toggle_output(); - return false; - } - }, - 'shift-o' : { - help : 'toggle output scrolling', - help_index : 'gc', - handler : function (event) { - that.notebook.toggle_output_scroll(); - return false; - } - }, - 's' : { - help : 'save notebook', - help_index : 'fa', - handler : function (event) { - that.notebook.save_checkpoint(); - return false; - } - }, - 'ctrl-j' : { - help : 'move cell down', - help_index : 'eb', - handler : function (event) { - that.notebook.move_cell_down(); - return false; - } - }, - 'ctrl-k' : { - help : 'move cell up', - help_index : 'ea', - handler : function (event) { - that.notebook.move_cell_up(); - return false; - } - }, - 'l' : { - help : 'toggle line numbers', - help_index : 'ga', - handler : function (event) { - that.notebook.cell_toggle_line_numbers(); - return false; - } - }, - 'i' : { - help : 'interrupt kernel (press twice)', - help_index : 'ha', - count: 2, - handler : function (event) { - that.notebook.kernel.interrupt(); - return false; - } - }, - '0' : { - help : 'restart kernel (press twice)', - help_index : 'hb', - count: 2, - handler : function (event) { - that.notebook.restart_kernel(); - return false; - } - }, - 'h' : { - help : 'keyboard shortcuts', - help_index : 'ge', - handler : function (event) { - that.quick_help.show_keyboard_shortcuts(); - return false; - } - }, - 'z' : { - help : 'undo last delete', - help_index : 'ei', - handler : function (event) { - that.notebook.undelete_cell(); - return false; - } - }, - 'shift-m' : { - help : 'merge cell below', - help_index : 'ek', - handler : function (event) { - that.notebook.merge_cell_below(); - return false; - } - }, - 'q' : { - help : 'close pager', - help_index : 'gd', - handler : function (event) { - that.pager.collapse(); - return false; - } + 'shift-space': 'ipython.scroll-up', + 'shift-v' : 'ipython.paste-cell-before', + 'shift-m' : 'ipython.merge-selected-cell-with-cell-after', + 'shift-o' : 'ipython.toggle-output-scrolling-selected-cell', + 'ctrl-j' : 'ipython.move-selected-cell-down', + 'ctrl-k' : 'ipython.move-selected-cell-up', + 'enter' : 'ipython.enter-edit-mode', + 'space' : 'ipython.scroll-down', + 'down' : 'ipython.select-next-cell', + 'i,i' : 'ipython.interrupt-kernel', + '0,0' : 'ipython.restart-kernel', + 'd,d' : 'ipython.delete-cell', + 'up' : 'ipython.select-previous-cell', + 'k' : 'ipython.select-previous-cell', + 'j' : 'ipython.select-next-cell', + 'x' : 'ipython.cut-selected-cell', + 'c' : 'ipython.copy-selected-cell', + 'v' : 'ipython.paste-cell-after', + 'a' : 'ipython.insert-cell-before', + 'b' : 'ipython.insert-cell-after', + 'y' : 'ipython.change-selected-cell-to-code-cell', + 'm' : 'ipython.change-selected-cell-to-markdown-cell', + 'r' : 'ipython.change-selected-cell-to-raw-cell', + '1' : 'ipython.change-selected-cell-to-heading-1', + '2' : 'ipython.change-selected-cell-to-heading-2', + '3' : 'ipython.change-selected-cell-to-heading-3', + '4' : 'ipython.change-selected-cell-to-heading-4', + '5' : 'ipython.change-selected-cell-to-heading-5', + '6' : 'ipython.change-selected-cell-to-heading-6', + 'o' : 'ipython.toggle-output-visibility-selected-cell', + 's' : 'ipython.save-notebook', + 'l' : 'ipython.toggle-line-number-selected-cell', + 'h' : 'ipython.show-keyboard-shortcut-help-dialog', + 'z' : 'ipython.undo-last-cell-deletion', + 'q' : 'ipython.close-pager', + 'i,e,e,e,e,e' : function(){console.log('[[===>>> 5E <<<===]]');}, + 'i,d,d,q,d' : function(){console.log('[[===>>> Trigger god mode <<<===]]');}, + 'i,d,d' : function(){console.log('[[===>>> should warn at registration <<<===]]');}, + 'i,d,k' : function(){console.log('[[===>>> Trigger shadow mode <<<===]]');}, + 'i,d,k,r,q' : function(){console.log('[[===>>> Trigger invisibility mode <<<===]]');}, + ';,up,down,up,down,left,right,left,right,b,a' : function(){console.log('[[===>>> Konami <<<===]]');}, + 'ctrl-x,meta-c,meta-b,u,t,t,e,r,f,l,y' : function(){ + console.log('[[Are you a real Programmer ?]]'); + window.open('http://xkcd.com/378/','_blank'); }, }; }; @@ -508,16 +136,27 @@ define([ KeyboardManager.prototype.bind_events = function () { var that = this; $(document).keydown(function (event) { - - if(event._ipkmIgnore==true||(event.originalEvent||{})._ipkmIgnore==true){ + if(event._ipkmIgnore===true||(event.originalEvent||{})._ipkmIgnore===true){ return false; } return that.handle_keydown(event); }); }; + KeyboardManager.prototype.set_notebook = function (notebook) { + this.notebook = notebook; + this.actions.extend_env({notebook:notebook}); + }; + + KeyboardManager.prototype.set_quickhelp = function (notebook) { + this.actions.extend_env({quick_help:notebook}); + }; + + KeyboardManager.prototype.handle_keydown = function (event) { - var notebook = this.notebook; + /** + * returning false from this will stop event propagation + **/ if (event.which === keycodes.esc) { // Intercept escape at highest level to avoid closing @@ -527,8 +166,7 @@ define([ if (!this.enabled) { if (event.which === keycodes.esc) { - // ESC - notebook.command_mode(); + this.notebook.command_mode(); return false; } return true; @@ -595,7 +233,8 @@ define([ }); }; - // For backwards compatability. + + // For backwards compatibility. IPython.KeyboardManager = KeyboardManager; return {'KeyboardManager': KeyboardManager}; diff --git a/IPython/html/static/notebook/js/main.js b/IPython/html/static/notebook/js/main.js index 5c41f4b..a9af555 100644 --- a/IPython/html/static/notebook/js/main.js +++ b/IPython/html/static/notebook/js/main.js @@ -16,6 +16,7 @@ require([ 'notebook/js/menubar', 'notebook/js/notificationarea', 'notebook/js/savewidget', + 'notebook/js/actions', 'notebook/js/keyboardmanager', 'notebook/js/config', 'notebook/js/kernelselector', @@ -36,7 +37,8 @@ require([ quickhelp, menubar, notificationarea, - savewidget, + savewidget, + actions, keyboardmanager, config, kernelselector, @@ -62,9 +64,11 @@ require([ var pager = new pager.Pager('div#pager', 'div#pager_splitter', { layout_manager: layout_manager, events: events}); + var acts = new actions.init(); var keyboard_manager = new keyboardmanager.KeyboardManager({ pager: pager, - events: events}); + events: events, + actions: acts }); var save_widget = new savewidget.SaveWidget('span#save_widget', { events: events, keyboard_manager: keyboard_manager}); @@ -77,11 +81,14 @@ require([ var login_widget = new loginwidget.LoginWidget('span#login_widget', common_options); var toolbar = new maintoolbar.MainToolBar('#maintoolbar-container', { notebook: notebook, - events: events}); + events: events, + actions: acts}); var quick_help = new quickhelp.QuickHelp({ keyboard_manager: keyboard_manager, events: events, notebook: notebook}); + keyboard_manager.set_notebook(notebook); + keyboard_manager.set_quickhelp(quick_help); var menubar = new menubar.MenuBar('#menubar', $.extend({ notebook: notebook, layout_manager: layout_manager, diff --git a/IPython/html/static/notebook/js/quickhelp.js b/IPython/html/static/notebook/js/quickhelp.js index 4bc179a..fe51f32 100644 --- a/IPython/html/static/notebook/js/quickhelp.js +++ b/IPython/html/static/notebook/js/quickhelp.js @@ -34,10 +34,10 @@ define([ platform_specific = [ { shortcut: "Cmd-Up", help:"go to cell start" }, { shortcut: "Cmd-Down", help:"go to cell end" }, - { shortcut: "Opt-Left", help:"go one word left" }, - { shortcut: "Opt-Right", help:"go one word right" }, - { shortcut: "Opt-Backspace", help:"del word before" }, - { shortcut: "Opt-Delete", help:"del word after" }, + { shortcut: "Alt-Left", help:"go one word left" }, + { shortcut: "Alt-Right", help:"go one word right" }, + { shortcut: "Alt-Backspace", help:"del word before" }, + { shortcut: "Alt-Delete", help:"del word after" }, ]; } else { // PC specific @@ -65,10 +65,6 @@ define([ ].concat( platform_specific ); - - - - QuickHelp.prototype.show_keyboard_shortcuts = function () { // toggles display of keyboard shortcut dialog var that = this; @@ -139,7 +135,9 @@ define([ keys[i] = "" + k + ""; continue; // leave individual keys lower-cased } - keys[i] = ( special_case[k] ? special_case[k] : k.charAt(0).toUpperCase() + k.slice(1) ); + if (k.indexOf(',') === -1){ + keys[i] = ( special_case[k] ? special_case[k] : k.charAt(0).toUpperCase() + k.slice(1) ); + } keys[i] = "" + keys[i] + ""; } return keys.join('-'); @@ -155,7 +153,12 @@ define([ var build_one = function (s) { var help = s.help; - var shortcut = prettify(s.shortcut); + var shortcut = ''; + if(s.shortcut){ + shortcut = prettify(s.shortcut); + } else { + console.error('[debug] - nothing for', s) + } return $('
').addClass('quickhelp'). append($('').addClass('shortcut_key').append($(shortcut))). append($('').addClass('shortcut_descr').text(' : ' + help)); diff --git a/IPython/html/static/notebook/js/toolbar.js b/IPython/html/static/notebook/js/toolbar.js index 4040659..5f6c192 100644 --- a/IPython/html/static/notebook/js/toolbar.js +++ b/IPython/html/static/notebook/js/toolbar.js @@ -11,7 +11,7 @@ define([ * A generic toolbar on which one can add button * @class ToolBar * @constructor - * @param {Dom_object} selector + * @param {Dom object} selector */ var ToolBar = function (selector, layout_manager) { this.selector = selector;