##// END OF EJS Templates
Add notion of action that differs from shortcuts....
Add notion of action that differs from shortcuts. This decouple the notion of shortcut from the notion of executed "action" This allow the shortcuts manager to be purely describe as data, and the same action to be later refered to either from the shortcut, from a toolbar button or a menu. This also implement a more complete keyboard shortcut handler which is able ton interpete sequences like `Cmd-X,Meta-v` By storing the shortcuts in a tree.

File last commit:

r18390:39ea1bc4
r18390:39ea1bc4
Show More
keyboard.js
454 lines | 15.9 KiB | application/javascript | JavascriptLexer
Jonathan Frederic
Almost done!...
r17198 // Copyright (c) IPython Development Team.
// Distributed under the terms of the Modified BSD License.
Matthias BUSSONNIER
Update to codemirror 4...
r18280 /**
*
*
* @module keyboard
* @namespace keyboard
* @class ShortcutManager
*/
Jonathan Frederic
Almost done!...
r17198
define([
'base/js/namespace',
Jonathan Frederic
MWE,...
r17200 'jquery',
Jonathan Frederic
Almost done!...
r17198 'base/js/utils',
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 'underscore',
], function(IPython, $, utils, _) {
Brian E. Granger
Creating new base/js/keyboard.js
r15615 "use strict";
Jonathan Frederic
Almost done!...
r17198
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 /**
* 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.
**/
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 // These apply to Firefox, (Webkit and IE)
// This does work **only** on US keyboard.
Brian E. Granger
Creating new base/js/keyboard.js
r15615 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
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
// This apply to Webkit and IE
var _ie_keycodes = {
Paul Ivanov
really fix the '-' key shortcuts now
r15877 '; :': 186, '= +': 187, '- _': 189
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Jonathan Frederic
Almost done!...
r17198 var browser = utils.browser[0];
var platform = utils.platform;
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Jonathan Frederic
Fix Gecko (Netscape) keyboard handling
r15875 if (browser === 'Firefox' || browser === 'Opera' || browser === 'Netscape') {
Brian E. Granger
Creating new base/js/keyboard.js
r15615 $.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) {
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 var n = names[0];
keycodes[n] = _keycodes[n];
inv_keycodes[_keycodes[n]] = n;
Brian E. Granger
Creating new base/js/keyboard.js
r15615 } else {
var primary = names[0];
var secondary = names[1];
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 keycodes[primary] = _keycodes[name];
keycodes[secondary] = _keycodes[name];
inv_keycodes[_keycodes[name]] = primary;
Brian E. Granger
Creating new base/js/keyboard.js
r15615 }
}
Brian E. Granger
Adding utility functions.
r15616 var normalize_key = function (key) {
return inv_keycodes[keycodes[key]];
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Adding utility functions.
r15616
var normalize_shortcut = function (shortcut) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 /**
* @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-');
}
Brian E. Granger
Adding utility functions.
r15616 shortcut = shortcut.toLowerCase().replace('cmd', 'meta');
Paul Ivanov
really fix the '-' key shortcuts now
r15877 shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 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
Paul Ivanov
use - for shortcut separators
r15873 var values = shortcut.split("-");
Brian E. Granger
Adding utility functions.
r15616 if (values.length === 1) {
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 return normalize_key(values[0]);
Brian E. Granger
Adding utility functions.
r15616 } else {
var modifiers = values.slice(0,-1);
var key = normalize_key(values[values.length-1]);
modifiers.sort();
Paul Ivanov
use - for shortcut separators
r15873 return modifiers.join('-') + '-' + key;
Brian E. Granger
Adding utility functions.
r15616 }
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Adding utility functions.
r15616
var shortcut_to_event = function (shortcut, type) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 /**
* Convert a shortcut (shift-r) to a jQuery Event object
**/
Brian E. Granger
Adding utility functions.
r15616 type = type || 'keydown';
shortcut = normalize_shortcut(shortcut);
Paul Ivanov
really fix the '-' key shortcuts now
r15877 shortcut = shortcut.replace(/-$/, '_'); // catch shortcuts using '-' key
Paul Ivanov
use - for shortcut separators
r15873 var values = shortcut.split("-");
Brian E. Granger
Adding utility functions.
r15616 var modifiers = values.slice(0,-1);
var key = values[values.length-1];
var opts = {which: keycodes[key]};
if (modifiers.indexOf('alt') !== -1) {opts.altKey = true;}
if (modifiers.indexOf('ctrl') !== -1) {opts.ctrlKey = true;}
if (modifiers.indexOf('meta') !== -1) {opts.metaKey = true;}
if (modifiers.indexOf('shift') !== -1) {opts.shiftKey = true;}
return $.Event(type, opts);
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Adding utility functions.
r15616
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 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'));
};
Brian E. Granger
Adding utility functions.
r15616 var event_to_shortcut = function (event) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 /**
* Convert a jQuery Event object to a normalized shortcut string (shift-r)
**/
Brian E. Granger
Adding utility functions.
r15616 var shortcut = '';
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 var key = inv_keycodes[event.which];
Paul Ivanov
use - for shortcut separators
r15873 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-';}
Brian E. Granger
Adding utility functions.
r15616 shortcut += key;
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 return shortcut;
};
Brian E. Granger
Adding utility functions.
r15616
Brian E. Granger
Creating new base/js/keyboard.js
r15615 // Shortcut manager class
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 var ShortcutManager = function (delay, events, actions, env) {
Matthias BUSSONNIER
Update to codemirror 4...
r18280 /**
* A class to deal with keyboard event and shortcut
*
* @class ShortcutManager
* @constructor
*/
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 this._shortcuts = {};
Brian E. Granger
Creating new base/js/keyboard.js
r15615 this.delay = delay || 800; // delay in milliseconds
Jonathan Frederic
Almost done!...
r17198 this.events = events;
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 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;
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
ShortcutManager.prototype.help = function () {
var help = [];
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 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;
Brian E. Granger
Creating new base/js/keyboard.js
r15615 if (help_string) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 var shortstring = (action.shortstring||shortcut);
Brian E. Granger
Creating new base/js/keyboard.js
r15615 help.push({
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 shortcut: shortstring,
Brian E. Granger
Creating new base/js/keyboard.js
r15615 help: help_string,
help_index: help_index}
);
}
}
help.sort(function (a, b) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 if (a.help_index > b.help_index){
Brian E. Granger
Creating new base/js/keyboard.js
r15615 return 1;
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 }
if (a.help_index < b.help_index){
Brian E. Granger
Creating new base/js/keyboard.js
r15615 return -1;
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 }
Brian E. Granger
Creating new base/js/keyboard.js
r15615 return 0;
});
return help;
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
ShortcutManager.prototype.clear_shortcuts = function () {
this._shortcuts = {};
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 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;
Brian E. Granger
Creating new base/js/keyboard.js
r15615 }
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 };
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);
Brian E. Granger
Creating new base/js/keyboard.js
r15615 }
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390
Brian E. Granger
Adding utility functions.
r15616 shortcut = normalize_shortcut(shortcut);
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 this.set_shortcut(shortcut, action_name);
Paul Ivanov
update notebook quickhelp on new shortcuts
r15769 if (!suppress_help_update) {
// update the keyboard shortcuts notebook help
Jonathan Frederic
Almost done!...
r17198 this.events.trigger('rebuild.QuickHelp');
Paul Ivanov
update notebook quickhelp on new shortcuts
r15769 }
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
ShortcutManager.prototype.add_shortcuts = function (data) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 /**
* Convenient methods to call `add_shortcut(key, value)` on several items
*
* data : Dict of the form {key:value, ...}
**/
Brian E. Granger
Creating new base/js/keyboard.js
r15615 for (var shortcut in data) {
Paul Ivanov
removing keyboard shortcuts should also update help
r15770 this.add_shortcut(shortcut, data[shortcut], true);
Brian E. Granger
Creating new base/js/keyboard.js
r15615 }
Paul Ivanov
update notebook quickhelp on new shortcuts
r15769 // update the keyboard shortcuts notebook help
Jonathan Frederic
Almost done!...
r17198 this.events.trigger('rebuild.QuickHelp');
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Paul Ivanov
removing keyboard shortcuts should also update help
r15770 ShortcutManager.prototype.remove_shortcut = function (shortcut, suppress_help_update) {
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 /**
* Remove the binding of shortcut `sortcut` with its action.
* throw an error if trying to remove a non-exiting shortcut
**/
Brian E. Granger
Adding utility functions.
r15616 shortcut = normalize_shortcut(shortcut);
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 if( typeof(shortcut) === 'string'){
shortcut = shortcut.split(',');
}
this._remove_leaf(shortcut, this._shortcuts);
Paul Ivanov
removing keyboard shortcuts should also update help
r15770 if (!suppress_help_update) {
// update the keyboard shortcuts notebook help
Jonathan Frederic
Almost done!...
r17198 this.events.trigger('rebuild.QuickHelp');
Paul Ivanov
removing keyboard shortcuts should also update help
r15770 }
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390
Brian E. Granger
Creating new base/js/keyboard.js
r15615
ShortcutManager.prototype.call_handler = function (event) {
Matthias BUSSONNIER
Update to codemirror 4...
r18280 /**
* Call the corresponding shortcut handler for a keyboard event
* @method call_handler
* @return {Boolean} `true|false`, `false` if no handler was found, otherwise the value return by the handler.
* @param event {event}
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 *
* 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;
}
Brian E. Granger
Adding utility functions.
r15616 var shortcut = event_to_shortcut(event);
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 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);
Brian E. Granger
Creating new base/js/keyboard.js
r15615 }
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390
return false;
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Brian E. Granger
Creating new base/js/keyboard.js
r15615
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390
Paul Ivanov
renamed 'use_shortcut' method to 'handles'
r15823 ShortcutManager.prototype.handles = function (event) {
Paul Ivanov
added new use_shortcut method to shortcuts...
r15758 var shortcut = event_to_shortcut(event);
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 var action_name = this.get_shortcut(this._queue.concat(shortcut));
return (typeof(action_name) !== 'undefined');
Jonathan Frederic
Almost done!...
r17198 };
Paul Ivanov
added new use_shortcut method to shortcuts...
r15758
Jonathan Frederic
More review changes
r17214 var keyboard = {
Brian E. Granger
Creating new base/js/keyboard.js
r15615 keycodes : keycodes,
inv_keycodes : inv_keycodes,
Brian E. Granger
Adding utility functions.
r15616 ShortcutManager : ShortcutManager,
normalize_key : normalize_key,
normalize_shortcut : normalize_shortcut,
shortcut_to_event : shortcut_to_event,
Matthias Bussonnier
Add notion of action that differs from shortcuts....
r18390 event_to_shortcut : event_to_shortcut,
Paul Ivanov
semicolon fixes buttress half of my js commits
r15840 };
Jonathan Frederic
More review changes
r17214
Matthias BUSSONNIER
Update to codemirror 4...
r18280 // For backwards compatibility.
Jonathan Frederic
More review changes
r17214 IPython.keyboard = keyboard;
return keyboard;
Jonathan Frederic
Almost done!...
r17198 });