Skip to content

Commit d8fc951

Browse files
committed
Merge pull request #1347 from Carreau/shortcut-editor-2
Create a shortcut editor for the notebook.
2 parents 6aa60a5 + baa49f7 commit d8fc951

File tree

15 files changed

+331
-51
lines changed

15 files changed

+331
-51
lines changed

.babelrc

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

.eslintignore

Lines changed: 5 additions & 0 deletions
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

Lines changed: 13 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 24 additions & 11 deletions
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

Lines changed: 6 additions & 0 deletions
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

Lines changed: 19 additions & 0 deletions
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

Lines changed: 30 additions & 28 deletions
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-
});
Lines changed: 173 additions & 0 deletions
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)