Merge pull request #6668 from Carreau/actions

Add notion of action to keyboard manager.
This commit is contained in:
Matthias Bussonnier 2014-11-22 12:23:16 +01:00
commit 5db4b7a8ee
7 changed files with 883 additions and 545 deletions

View File

@ -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.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;
}
};
ShortcutManager.prototype.add_shortcut = function (shortcut, data, suppress_help_update) {
if (typeof(data) === 'function') {
data = {help: '', help_index: '', handler: data};
}
data.help_index = data.help_index || '';
data.help = data.help || '';
data.count = data.count || 1;
if (data.help_index === '') {
data.help_index = 'zz';
/**
* 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}
*/
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);
}
}
*
* 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;
}
return true;
var shortcut = event_to_shortcut(event);
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 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.

View File

@ -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};
});

View File

@ -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,487 +34,119 @@ 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',
};
};
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 +156,7 @@ define([
if (!this.enabled) {
if (event.which === keycodes.esc) {
// ESC
notebook.command_mode();
this.notebook.command_mode();
return false;
}
return true;
@ -595,7 +223,8 @@ define([
});
};
// For backwards compatability.
// For backwards compatibility.
IPython.KeyboardManager = KeyboardManager;
return {'KeyboardManager': KeyboardManager};

View File

@ -17,6 +17,7 @@ require([
'notebook/js/menubar',
'notebook/js/notificationarea',
'notebook/js/savewidget',
'notebook/js/actions',
'notebook/js/keyboardmanager',
'notebook/js/config',
'notebook/js/kernelselector',
@ -39,7 +40,8 @@ require([
quickhelp,
menubar,
notificationarea,
savewidget,
savewidget,
actions,
keyboardmanager,
config,
kernelselector,
@ -66,9 +68,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});
@ -85,11 +89,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,
contents: contents,

View File

@ -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] = "<code><strong>" + k + "</strong></code>";
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] = "<code><strong>" + keys[i] + "</strong></code>";
}
return keys.join('-');

View File

@ -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;

View File

@ -13,7 +13,9 @@ var to_normalize = [
var unshifted = "` 1 2 3 4 5 6 7 8 9 0 - = q w e r t y u i o p [ ] \\ a s d f g h j k l ; ' z x c v b n m , . /";
// shifted = '~ ! @ # $ % ^ & * ( ) _ + Q W E R T Y U I O P { } | A S D F G H J K L : " Z X C V B N M < > ?';
casper.notebook_test(function () {
var that = this;
this.then(function () {
this.each(unshifted.split(' '), function (self, item) {
@ -46,4 +48,37 @@ casper.notebook_test(function () {
});
});
this.then(function(){
var shortcuts_test = {
'i,e,e,e,e,e' : '[[5E]]',
'i,d,d,q,d' : '[[TEST1]]',
'i,d,d' : '[[TEST1 WILL BE SHADOWED]]',
'i,d,k' : '[[SHOULD SHADOW TEST2]]',
'i,d,k,r,q' : '[[TEST2 NOT SHADOWED]]',
';,up,down,up,down,left,right,left,right,b,a' : '[[KONAMI]]',
'ctrl-x,meta-c,meta-b,u,t,t,e,r,f,l,y' : '[[XKCD]]'
};
that.msgs = [];
that.on('remote.message', function(msg) {
that.msgs.push(msg);
})
that.evaluate(function (obj) {
for(var k in obj){
IPython.keyboard_manager.command_shortcuts.add_shortcut(k, function(){console.log(obj[k])});
}
}, shortcuts_test);
var longer_first = false;
var longer_last = false;
for(var m in that.msgs){
longer_first = longer_first||(that.msgs[m].match(/you are overriting/)!= null);
longer_last = longer_last ||(that.msgs[m].match(/will be shadowed/) != null);
}
this.test.assert(longer_first, 'no warnign if registering shorter shortut');
this.test.assert(longer_last , 'no warnign if registering longer shortut');
})
});