Skip to content

Commit ab85f16

Browse files
committed
Create shortcut editor for the notebook
1) finish the step allowing the use of es6 - this include some tweak to web pack configuration to speed up recompile in watch mode (in particular cache sourcemaps). - enable eslint (error only), on obvious mistakes. - setup babel to compile to es5 as a target. 2) Make the test pass under Casper that does not always have `Function.prototype.bind` defined, which we cannot patch only in the tests. 3) Write an actual shortcut editor that list and allow to modify most of the command mode shortcut. The logic to persist the shortcuts is a bit tricky as there are default keyboard shortcuts, and so when you "unbind" them you need to re-unbind them at next startup. This does not work for a few shortcut for technical reasons: `<Esc>`, `<Shift>`, as well as `<Ctrl-Shift-P>` and `<F>` which register asynchronously, so are not detected as "default" shortcuts.
1 parent 89f9420 commit ab85f16

15 files changed

+324
-51
lines changed

.babelrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": ["es2015"],
3+
}

.eslintignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.min.js
2+
*components*
3+
*node_modules*
4+
*built*
5+
*build*

.eslintrc.json

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"parserOptions": {
3+
"ecmaVersion": 6,
4+
"sourceType": "module"
5+
},
6+
"rules": {
7+
"semi": 1,
8+
"no-cond-assign": 2,
9+
"no-debugger": 2,
10+
"comma-dangle": 1,
11+
"no-unreachable" : 2
12+
}
13+
}

.travis.yml

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ install:
3636

3737
script:
3838
- 'if [[ $GROUP == js* ]]; then travis_retry python -m notebook.jstest ${GROUP:3}; fi'
39+
- 'if [[ $GROUP == "js/notebook" ]]; then npm run lint; fi'
3940
- 'if [[ $GROUP == python ]]; then nosetests -v --with-coverage --cover-package=notebook notebook; fi'
4041

4142
matrix:

notebook/static/base/js/keyboard.js

+24-11
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,21 @@ define([
231231
}
232232
return dct;
233233
};
234+
235+
236+
ShortcutManager.prototype.get_action_shortcuts = function(name){
237+
var ftree = flatten_shorttree(this._shortcuts);
238+
var res = [];
239+
for (var sht in ftree ){
240+
if(ftree[sht] === name){
241+
res.push(sht);
242+
}
243+
}
244+
return res;
245+
};
234246

235247
ShortcutManager.prototype.get_action_shortcut = function(name){
236248
var ftree = flatten_shorttree(this._shortcuts);
237-
var res = {};
238249
for (var sht in ftree ){
239250
if(ftree[sht] === name){
240251
return sht;
@@ -405,25 +416,27 @@ define([
405416

406417
shortcut = shortcut.toLowerCase();
407418
this.remove_shortcut(shortcut);
408-
var patch = {keys:{}};
409-
var b = {bind:{}};
419+
const patch = {keys:{}};
420+
const b = {bind:{}};
410421
patch.keys[this._mode] = {bind:{}};
411422
patch.keys[this._mode].bind[shortcut] = null;
412423
this._config.update(patch);
413424

414425
// if the shortcut we unbind is a default one, we add it to the list of
415426
// things to unbind at startup
427+
if( this._defaults_bindings.indexOf(shortcut) !== -1 ){
428+
const cnf = (this._config.data.keys||{})[this._mode];
429+
const unbind_array = cnf.unbind||[];
416430

417-
if(this._defaults_bindings.indexOf(shortcut) !== -1){
418-
var cnf = (this._config.data.keys||{})[this._mode];
419-
var unbind_array = cnf.unbind||[];
420431

421432
// unless it's already there (like if we have remapped a default
422-
// shortcut to another command, and unbind it)
423-
if(unbind_array.indexOf(shortcut) !== -1){
424-
unbind_array.concat(shortcut);
425-
var unbind_patch = {keys:{unbind:unbind_array}};
426-
this._config._update(unbind_patch);
433+
// shortcut to another command): unbind it)
434+
if(unbind_array.indexOf(shortcut) === -1){
435+
const _parray = unbind_array.concat(shortcut);
436+
const unbind_patch = {keys:{}};
437+
unbind_patch.keys[this._mode] = {unbind:_parray}
438+
console.warn('up:', unbind_patch);
439+
this._config.update(unbind_patch);
427440
}
428441
}
429442
};

notebook/static/notebook/js/actions.js

+6
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ define(function(require){
6262
*
6363
**/
6464
var _actions = {
65+
'edit-command-mode-keyboard-shortcuts': {
66+
help: 'Open a dialog to edit the command mode keyboard shortcuts',
67+
handler: function (env) {
68+
env.notebook.show_shortcuts_editor();
69+
}
70+
},
6571
'restart-kernel': {
6672
help: 'restart the kernel (no confirmation dialog)',
6773
handler: function (env) {

notebook/static/notebook/js/main.js

+19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,25 @@
22
// Distributed under the terms of the Modified BSD License.
33
__webpack_public_path__ = window['staticURL'] + 'notebook/js/built/';
44

5+
// adapted from Mozilla Developer Network example at
6+
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
7+
// shim `bind` for testing under casper.js
8+
var bind = function bind(obj) {
9+
var slice = [].slice;
10+
var args = slice.call(arguments, 1),
11+
self = this,
12+
nop = function() {
13+
},
14+
bound = function() {
15+
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
16+
};
17+
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
18+
bound.prototype = new nop();
19+
return bound;
20+
};
21+
Function.prototype.bind = Function.prototype.bind || bind ;
22+
23+
524
requirejs(['contents'], function(contentsModule) {
625
require([
726
'base/js/namespace',

notebook/static/notebook/js/notebook.js

+30-28
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,31 @@
44
/**
55
* @module notebook
66
*/
7-
define(function (require) {
8-
"use strict";
9-
var IPython = require('base/js/namespace');
10-
var _ = require('underscore');
11-
var utils = require('base/js/utils');
12-
var dialog = require('base/js/dialog');
13-
var cellmod = require('notebook/js/cell');
14-
var textcell = require('notebook/js/textcell');
15-
var codecell = require('notebook/js/codecell');
16-
var moment = require('moment');
17-
var configmod = require('services/config');
18-
var session = require('services/sessions/session');
19-
var celltoolbar = require('notebook/js/celltoolbar');
20-
var marked = require('components/marked/lib/marked');
21-
var CodeMirror = require('codemirror/lib/codemirror');
22-
var runMode = require('codemirror/addon/runmode/runmode');
23-
var mathjaxutils = require('notebook/js/mathjaxutils');
24-
var keyboard = require('base/js/keyboard');
25-
var tooltip = require('notebook/js/tooltip');
26-
var default_celltoolbar = require('notebook/js/celltoolbarpresets/default');
27-
var rawcell_celltoolbar = require('notebook/js/celltoolbarpresets/rawcell');
28-
var slideshow_celltoolbar = require('notebook/js/celltoolbarpresets/slideshow');
29-
var attachments_celltoolbar = require('notebook/js/celltoolbarpresets/attachments');
30-
var scrollmanager = require('notebook/js/scrollmanager');
31-
var commandpalette = require('notebook/js/commandpalette');
7+
"use strict";
8+
import IPython from 'base/js/namespace';
9+
import _ from 'underscore';
10+
import utils from 'base/js/utils';
11+
import dialog from 'base/js/dialog';
12+
import cellmod from 'notebook/js/cell';
13+
import textcell from 'notebook/js/textcell';
14+
import codecell from 'notebook/js/codecell';
15+
import moment from 'moment';
16+
import configmod from 'services/config';
17+
import session from 'services/sessions/session';
18+
import celltoolbar from 'notebook/js/celltoolbar';
19+
import marked from 'components/marked/lib/marked';
20+
import CodeMirror from 'codemirror/lib/codemirror';
21+
import runMode from 'codemirror/addon/runmode/runmode';
22+
import mathjaxutils from 'notebook/js/mathjaxutils';
23+
import keyboard from 'base/js/keyboard';
24+
import tooltip from 'notebook/js/tooltip';
25+
import default_celltoolbar from 'notebook/js/celltoolbarpresets/default';
26+
import rawcell_celltoolbar from 'notebook/js/celltoolbarpresets/rawcell';
27+
import slideshow_celltoolbar from 'notebook/js/celltoolbarpresets/slideshow';
28+
import attachments_celltoolbar from 'notebook/js/celltoolbarpresets/attachments';
29+
import scrollmanager from 'notebook/js/scrollmanager';
30+
import commandpalette from 'notebook/js/commandpalette';
31+
import {ShortcutEditor} from 'notebook/js/shortcuteditor';
3232

3333
var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';
3434

@@ -50,7 +50,7 @@ define(function (require) {
5050
* @param {string} options.notebook_path
5151
* @param {string} options.notebook_name
5252
*/
53-
var Notebook = function (selector, options) {
53+
export function Notebook(selector, options) {
5454
this.config = options.config;
5555
this.class_config = new configmod.ConfigWithDefaults(this.config,
5656
Notebook.options_default, 'Notebook');
@@ -362,6 +362,10 @@ define(function (require) {
362362
var x = new commandpalette.CommandPalette(this);
363363
};
364364

365+
Notebook.prototype.show_shortcuts_editor = function() {
366+
new ShortcutEditor(this);
367+
};
368+
365369
/**
366370
* Trigger a warning dialog about missing functionality from newer minor versions
367371
*/
@@ -3160,5 +3164,3 @@ define(function (require) {
31603164
this.load_notebook(this.notebook_path);
31613165
};
31623166

3163-
return {'Notebook': Notebook};
3164-
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
5+
import QH from "notebook/js/quickhelp";
6+
import dialog from "base/js/dialog";
7+
import {render} from "preact";
8+
import {createElement, createClass} from "preact-compat";
9+
import marked from 'components/marked/lib/marked';
10+
11+
/**
12+
* Humanize the action name to be consumed by user.
13+
* internally the actions name are of the form
14+
* <namespace>:<description-with-dashes>
15+
* we drop <namespace> and replace dashes for space.
16+
*/
17+
const humanize_action_id = function(str) {
18+
return str.split(':')[1].replace(/-/g, ' ').replace(/_/g, '-');
19+
};
20+
21+
/**
22+
* given an action id return 'command-shortcut', 'edit-shortcut' or 'no-shortcut'
23+
* for the action. This allows us to tag UI in order to visually distinguish
24+
* Wether an action have a keybinding or not.
25+
**/
26+
27+
const KeyBinding = createClass({
28+
displayName: 'KeyBindings',
29+
getInitialState: function() {
30+
return {shrt:''};
31+
},
32+
handleShrtChange: function (element){
33+
this.setState({shrt:element.target.value});
34+
},
35+
render: function(){
36+
const that = this;
37+
const available = this.props.available(this.state.shrt);
38+
const empty = (this.state.shrt === '');
39+
return createElement('div', {className:'jupyter-keybindings'},
40+
createElement('i', {className: "pull-right fa fa-plus", alt: 'add-keyboard-shortcut',
41+
onClick:()=>{
42+
available?that.props.onAddBindings(that.state.shrt, that.props.ckey):null;
43+
that.state.shrt='';
44+
}
45+
}),
46+
createElement('input', {
47+
type:'text',
48+
placeholder:'add shortcut',
49+
className:'pull-right'+((available||empty)?'':' alert alert-danger'),
50+
value:this.state.shrt,
51+
onChange:this.handleShrtChange
52+
}),
53+
this.props.shortcuts?this.props.shortcuts.map((item, index) => {
54+
return createElement('span', {className: 'pull-right'},
55+
createElement('kbd', {}, [
56+
item.h,
57+
createElement('i', {className: "fa fa-times", alt: 'remove '+item.h,
58+
onClick:()=>{
59+
that.props.unbind(item.raw);
60+
}
61+
})
62+
])
63+
);
64+
}):null,
65+
createElement('div', {title: '(' +this.props.ckey + ')' , className:'jupyter-keybindings-text'}, this.props.display )
66+
);
67+
}
68+
});
69+
70+
const KeyBindingList = createClass({
71+
displayName: 'KeyBindingList',
72+
getInitialState: function(){
73+
return {data:[]};
74+
},
75+
componentDidMount: function(){
76+
this.setState({data:this.props.callback()});
77+
},
78+
render: function() {
79+
const childrens = this.state.data.map((binding)=>{
80+
return createElement(KeyBinding, Object.assign({}, binding, {onAddBindings:(shortcut, action)=>{
81+
this.props.bind(shortcut, action);
82+
this.setState({data:this.props.callback()});
83+
},
84+
available:this.props.available,
85+
unbind: (shortcut) => {
86+
this.props.unbind(shortcut);
87+
this.setState({data:this.props.callback()});
88+
}
89+
}));
90+
});
91+
childrens.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML:
92+
{__html:
93+
marked(
94+
95+
"This dialog allows you to modify the keymap of the command mode, and persist the changes. "+
96+
"You can define many type of shorctuts and sequences of keys. "+
97+
"\n\n"+
98+
" - Use dashes `-` to represent keys that should be pressed with modifiers, "+
99+
"for example `Shift-a`, or `Ctrl-;`. \n"+
100+
" - Separate by commas if the keys need to be pressed in sequence: `h,a,l,t`.\n"+
101+
"\n\nYou can combine the two: `Ctrl-x,Meta-c,Meta-b,u,t,t,e,r,f,l,y`.\n"+
102+
"Casing will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+
103+
" You need to explicitelty write the `Shift` modifier.\n"+
104+
"Valid modifiers are `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. Refer to developper docs "+
105+
"for their signification depending on the platform."
106+
)}
107+
}));
108+
return createElement('div',{}, childrens);
109+
}
110+
});
111+
112+
const get_shortcuts_data = function(notebook) {
113+
const actions = Object.keys(notebook.keyboard_manager.actions._actions);
114+
const src = [];
115+
116+
for (let i = 0; i < actions.length; i++) {
117+
const action_id = actions[i];
118+
const action = notebook.keyboard_manager.actions.get(action_id);
119+
120+
let shortcuts = notebook.keyboard_manager.command_shortcuts.get_action_shortcuts(action_id);
121+
let hshortcuts;
122+
if (shortcuts.length > 0) {
123+
hshortcuts = shortcuts.map((raw)=>{
124+
return {h:QH._humanize_sequence(raw),raw:raw};}
125+
);
126+
}
127+
src.push({
128+
display: humanize_action_id(action_id),
129+
shortcuts: hshortcuts,
130+
key:action_id, // react specific thing
131+
ckey: action_id
132+
});
133+
}
134+
return src;
135+
};
136+
137+
138+
export const ShortcutEditor = function(notebook) {
139+
140+
if(!notebook){
141+
throw new Error("CommandPalette takes a notebook non-null mandatory arguement");
142+
}
143+
144+
const body = $('<div>');
145+
const mod = dialog.modal({
146+
notebook: notebook,
147+
keyboard_manager: notebook.keyboard_manager,
148+
title : "Edit Command mode Shortcuts",
149+
body : body,
150+
buttons : {
151+
OK : {}
152+
}
153+
});
154+
155+
const src = get_shortcuts_data(notebook);
156+
157+
mod.addClass("modal_stretch");
158+
159+
mod.modal('show');
160+
render(
161+
createElement(KeyBindingList, {
162+
callback:()=>{ return get_shortcuts_data(notebook);},
163+
bind: (shortcut, command) => {
164+
return notebook.keyboard_manager.command_shortcuts._persist_shortcut(shortcut, command);
165+
},
166+
unbind: (shortcut) => {
167+
return notebook.keyboard_manager.command_shortcuts._persist_remove_shortcut(shortcut);
168+
},
169+
available: (shrt) => { return notebook.keyboard_manager.command_shortcuts.is_available_shortcut(shrt);}
170+
}),
171+
body.get(0)
172+
);
173+
};

0 commit comments

Comments
 (0)