Merge pull request #1347 from Carreau/shortcut-editor-2

Create a shortcut editor for the notebook.
This commit is contained in:
Matthias Bussonnier 2016-05-05 14:47:09 -07:00
commit d8fc95173b
15 changed files with 331 additions and 51 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"presets": ["es2015"],
}

5
.eslintignore Normal file
View File

@ -0,0 +1,5 @@
*.min.js
*components*
*node_modules*
*built*
*build*

13
.eslintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"rules": {
"semi": 1,
"no-cond-assign": 2,
"no-debugger": 2,
"comma-dangle": 1,
"no-unreachable" : 2
}
}

View File

@ -36,6 +36,7 @@ install:
script:
- 'if [[ $GROUP == js* ]]; then travis_retry python -m notebook.jstest ${GROUP:3}; fi'
- 'if [[ $GROUP == "js/notebook" ]]; then npm run lint; fi'
- 'if [[ $GROUP == python ]]; then nosetests -v --with-coverage --cover-package=notebook notebook; fi'
matrix:

View File

@ -231,10 +231,21 @@ define([
}
return dct;
};
ShortcutManager.prototype.get_action_shortcuts = function(name){
var ftree = flatten_shorttree(this._shortcuts);
var res = [];
for (var sht in ftree ){
if(ftree[sht] === name){
res.push(sht);
}
}
return res;
};
ShortcutManager.prototype.get_action_shortcut = function(name){
var ftree = flatten_shorttree(this._shortcuts);
var res = {};
for (var sht in ftree ){
if(ftree[sht] === name){
return sht;
@ -405,25 +416,27 @@ define([
shortcut = shortcut.toLowerCase();
this.remove_shortcut(shortcut);
var patch = {keys:{}};
var b = {bind:{}};
const patch = {keys:{}};
const b = {bind:{}};
patch.keys[this._mode] = {bind:{}};
patch.keys[this._mode].bind[shortcut] = null;
this._config.update(patch);
// if the shortcut we unbind is a default one, we add it to the list of
// things to unbind at startup
if( this._defaults_bindings.indexOf(shortcut) !== -1 ){
const cnf = (this._config.data.keys||{})[this._mode];
const unbind_array = cnf.unbind||[];
if(this._defaults_bindings.indexOf(shortcut) !== -1){
var cnf = (this._config.data.keys||{})[this._mode];
var unbind_array = cnf.unbind||[];
// unless it's already there (like if we have remapped a default
// shortcut to another command, and unbind it)
if(unbind_array.indexOf(shortcut) !== -1){
unbind_array.concat(shortcut);
var unbind_patch = {keys:{unbind:unbind_array}};
this._config._update(unbind_patch);
// shortcut to another command): unbind it)
if(unbind_array.indexOf(shortcut) === -1){
const _parray = unbind_array.concat(shortcut);
const unbind_patch = {keys:{}};
unbind_patch.keys[this._mode] = {unbind:_parray}
console.warn('up:', unbind_patch);
this._config.update(unbind_patch);
}
}
};

View File

@ -62,6 +62,12 @@ define(function(require){
*
**/
var _actions = {
'edit-command-mode-keyboard-shortcuts': {
help: 'Open a dialog to edit the command mode keyboard shortcuts',
handler: function (env) {
env.notebook.show_shortcuts_editor();
}
},
'restart-kernel': {
help: 'restart the kernel (no confirmation dialog)',
handler: function (env) {

View File

@ -2,6 +2,25 @@
// Distributed under the terms of the Modified BSD License.
__webpack_public_path__ = window['staticURL'] + 'notebook/js/built/';
// adapted from Mozilla Developer Network example at
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
// shim `bind` for testing under casper.js
var bind = function bind(obj) {
var slice = [].slice;
var args = slice.call(arguments, 1),
self = this,
nop = function() {
},
bound = function() {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
};
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
bound.prototype = new nop();
return bound;
};
Function.prototype.bind = Function.prototype.bind || bind ;
requirejs(['contents'], function(contentsModule) {
require([
'base/js/namespace',

View File

@ -4,31 +4,31 @@
/**
* @module notebook
*/
define(function (require) {
"use strict";
var IPython = require('base/js/namespace');
var _ = require('underscore');
var utils = require('base/js/utils');
var dialog = require('base/js/dialog');
var cellmod = require('notebook/js/cell');
var textcell = require('notebook/js/textcell');
var codecell = require('notebook/js/codecell');
var moment = require('moment');
var configmod = require('services/config');
var session = require('services/sessions/session');
var celltoolbar = require('notebook/js/celltoolbar');
var marked = require('components/marked/lib/marked');
var CodeMirror = require('codemirror/lib/codemirror');
var runMode = require('codemirror/addon/runmode/runmode');
var mathjaxutils = require('notebook/js/mathjaxutils');
var keyboard = require('base/js/keyboard');
var tooltip = require('notebook/js/tooltip');
var default_celltoolbar = require('notebook/js/celltoolbarpresets/default');
var rawcell_celltoolbar = require('notebook/js/celltoolbarpresets/rawcell');
var slideshow_celltoolbar = require('notebook/js/celltoolbarpresets/slideshow');
var attachments_celltoolbar = require('notebook/js/celltoolbarpresets/attachments');
var scrollmanager = require('notebook/js/scrollmanager');
var commandpalette = require('notebook/js/commandpalette');
"use strict";
import IPython from 'base/js/namespace';
import _ from 'underscore';
import utils from 'base/js/utils';
import dialog from 'base/js/dialog';
import cellmod from 'notebook/js/cell';
import textcell from 'notebook/js/textcell';
import codecell from 'notebook/js/codecell';
import moment from 'moment';
import configmod from 'services/config';
import session from 'services/sessions/session';
import celltoolbar from 'notebook/js/celltoolbar';
import marked from 'components/marked/lib/marked';
import CodeMirror from 'codemirror/lib/codemirror';
import runMode from 'codemirror/addon/runmode/runmode';
import mathjaxutils from 'notebook/js/mathjaxutils';
import keyboard from 'base/js/keyboard';
import tooltip from 'notebook/js/tooltip';
import default_celltoolbar from 'notebook/js/celltoolbarpresets/default';
import rawcell_celltoolbar from 'notebook/js/celltoolbarpresets/rawcell';
import slideshow_celltoolbar from 'notebook/js/celltoolbarpresets/slideshow';
import attachments_celltoolbar from 'notebook/js/celltoolbarpresets/attachments';
import scrollmanager from 'notebook/js/scrollmanager';
import commandpalette from 'notebook/js/commandpalette';
import {ShortcutEditor} from 'notebook/js/shortcuteditor';
var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';
@ -50,7 +50,7 @@ define(function (require) {
* @param {string} options.notebook_path
* @param {string} options.notebook_name
*/
var Notebook = function (selector, options) {
export function Notebook(selector, options) {
this.config = options.config;
this.class_config = new configmod.ConfigWithDefaults(this.config,
Notebook.options_default, 'Notebook');
@ -362,6 +362,10 @@ define(function (require) {
var x = new commandpalette.CommandPalette(this);
};
Notebook.prototype.show_shortcuts_editor = function() {
new ShortcutEditor(this);
};
/**
* Trigger a warning dialog about missing functionality from newer minor versions
*/
@ -3160,5 +3164,3 @@ define(function (require) {
this.load_notebook(this.notebook_path);
};
return {'Notebook': Notebook};
});

View File

@ -0,0 +1,173 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import QH from "notebook/js/quickhelp";
import dialog from "base/js/dialog";
import {render} from "preact";
import {createElement, createClass} from "preact-compat";
import marked from 'components/marked/lib/marked';
/**
* Humanize the action name to be consumed by user.
* internally the actions name are of the form
* <namespace>:<description-with-dashes>
* we drop <namespace> and replace dashes for space.
*/
const humanize_action_id = function(str) {
return str.split(':')[1].replace(/-/g, ' ').replace(/_/g, '-');
};
/**
* given an action id return 'command-shortcut', 'edit-shortcut' or 'no-shortcut'
* for the action. This allows us to tag UI in order to visually distinguish
* Wether an action have a keybinding or not.
**/
const KeyBinding = createClass({
displayName: 'KeyBindings',
getInitialState: function() {
return {shrt:''};
},
handleShrtChange: function (element){
this.setState({shrt:element.target.value});
},
render: function(){
const that = this;
const available = this.props.available(this.state.shrt);
const empty = (this.state.shrt === '');
return createElement('div', {className:'jupyter-keybindings'},
createElement('i', {className: "pull-right fa fa-plus", alt: 'add-keyboard-shortcut',
onClick:()=>{
available?that.props.onAddBindings(that.state.shrt, that.props.ckey):null;
that.state.shrt='';
}
}),
createElement('input', {
type:'text',
placeholder:'add shortcut',
className:'pull-right'+((available||empty)?'':' alert alert-danger'),
value:this.state.shrt,
onChange:this.handleShrtChange
}),
this.props.shortcuts?this.props.shortcuts.map((item, index) => {
return createElement('span', {className: 'pull-right'},
createElement('kbd', {}, [
item.h,
createElement('i', {className: "fa fa-times", alt: 'remove '+item.h,
onClick:()=>{
that.props.unbind(item.raw);
}
})
])
);
}):null,
createElement('div', {title: '(' +this.props.ckey + ')' , className:'jupyter-keybindings-text'}, this.props.display )
);
}
});
const KeyBindingList = createClass({
displayName: 'KeyBindingList',
getInitialState: function(){
return {data:[]};
},
componentDidMount: function(){
this.setState({data:this.props.callback()});
},
render: function() {
const childrens = this.state.data.map((binding)=>{
return createElement(KeyBinding, Object.assign({}, binding, {onAddBindings:(shortcut, action)=>{
this.props.bind(shortcut, action);
this.setState({data:this.props.callback()});
},
available:this.props.available,
unbind: (shortcut) => {
this.props.unbind(shortcut);
this.setState({data:this.props.callback()});
}
}));
});
childrens.unshift(createElement('div', {className:'well', key:'disclamer', dangerouslySetInnerHTML:
{__html:
marked(
"This dialog allows you to modify the keymap of the command mode, and persist the changes. "+
"You can define many type of shorctuts and sequences of keys. "+
"\n\n"+
" - Use dashes `-` to represent keys that should be pressed with modifiers, "+
"for example `Shift-a`, or `Ctrl-;`. \n"+
" - Separate by commas if the keys need to be pressed in sequence: `h,a,l,t`.\n"+
"\n\nYou can combine the two: `Ctrl-x,Meta-c,Meta-b,u,t,t,e,r,f,l,y`.\n"+
"Casing will have no effects: (e.g: `;` and `:` are the same on english keyboards)."+
" You need to explicitelty write the `Shift` modifier.\n"+
"Valid modifiers are `Cmd`, `Ctrl`, `Alt` ,`Meta`, `Cmdtrl`. Refer to developper docs "+
"for their signification depending on the platform."
)}
}));
return createElement('div',{}, childrens);
}
});
const get_shortcuts_data = function(notebook) {
const actions = Object.keys(notebook.keyboard_manager.actions._actions);
const src = [];
for (let i = 0; i < actions.length; i++) {
const action_id = actions[i];
const action = notebook.keyboard_manager.actions.get(action_id);
let shortcuts = notebook.keyboard_manager.command_shortcuts.get_action_shortcuts(action_id);
let hshortcuts;
if (shortcuts.length > 0) {
hshortcuts = shortcuts.map((raw)=>{
return {h:QH._humanize_sequence(raw),raw:raw};}
);
}
src.push({
display: humanize_action_id(action_id),
shortcuts: hshortcuts,
key:action_id, // react specific thing
ckey: action_id
});
}
return src;
};
export const ShortcutEditor = function(notebook) {
if(!notebook){
throw new Error("CommandPalette takes a notebook non-null mandatory arguement");
}
const body = $('<div>');
const mod = dialog.modal({
notebook: notebook,
keyboard_manager: notebook.keyboard_manager,
title : "Edit Command mode Shortcuts",
body : body,
buttons : {
OK : {}
}
});
const src = get_shortcuts_data(notebook);
mod.addClass("modal_stretch");
mod.modal('show');
render(
createElement(KeyBindingList, {
callback:()=>{ return get_shortcuts_data(notebook);},
bind: (shortcut, command) => {
return notebook.keyboard_manager.command_shortcuts._persist_shortcut(shortcut, command);
},
unbind: (shortcut) => {
return notebook.keyboard_manager.command_shortcuts._persist_remove_shortcut(shortcut);
},
available: (shrt) => { return notebook.keyboard_manager.command_shortcuts.is_available_shortcut(shrt);}
}),
body.get(0)
);
};

View File

@ -99,3 +99,19 @@ kbd {
padding-top: 1px;
padding-bottom: 1px;
}
.jupyter-keybindings {
padding: 0px;
line-height: 24px;
border-bottom: 1px solid gray;
}
.jupyter-keybindings input {
margin: 0;
padding: 0;
border: none;
}
.jupyter-keybindings i {
padding: 6px;
}

View File

@ -2,6 +2,25 @@
// Distributed under the terms of the Modified BSD License.
__webpack_public_path__ = window['staticURL'] + 'tree/js/built/';
// adapted from Mozilla Developer Network example at
// https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind
// shim `bind` for testing under casper.js
var bind = function bind(obj) {
var slice = [].slice;
var args = slice.call(arguments, 1),
self = this,
nop = function() {
},
bound = function() {
return self.apply(this instanceof nop ? this : (obj || {}), args.concat(slice.call(arguments)));
};
nop.prototype = this.prototype || {}; // Firefox cries sometimes if prototype is undefined
bound.prototype = new nop();
return bound;
};
Function.prototype.bind = Function.prototype.bind || bind ;
requirejs(['contents'], function(contents_service) {
require([
'base/js/namespace',

View File

@ -1,14 +1,14 @@
casper.notebook_test(function () {
// Note, \033 is the octal notation of \u001b
// Note, \u001b is the unicode notation of octal \033 which is not officially in js
var input = [
"\033[0m[\033[0minfo\033[0m] \033[0mtext\033[0m",
"\033[0m[\033[33mwarn\033[0m] \033[0m\tmore text\033[0m",
"\033[0m[\033[33mwarn\033[0m] \033[0m https://some/url/to/a/file.ext\033[0m",
"\033[0m[\033[31merror\033[0m] \033[0m\033[0m",
"\033[0m[\033[31merror\033[0m] \033[0m\teven more text\033[0m",
"\u001b[0m[\u001b[0minfo\u001b[0m] \u001b[0mtext\u001b[0m",
"\u001b[0m[\u001b[33mwarn\u001b[0m] \u001b[0m\tmore text\u001b[0m",
"\u001b[0m[\u001b[33mwarn\u001b[0m] \u001b[0m https://some/url/to/a/file.ext\u001b[0m",
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\u001b[0m",
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\teven more text\u001b[0m",
"\u001b[?25hBuilding wheels for collected packages: scipy",
"\x1b[38;5;28;01mtry\x1b[39;00m",
"\033[0m[\033[31merror\033[0m] \033[0m\t\tand more more text\033[0m",
"\u001b[0m[\u001b[31merror\u001b[0m] \u001b[0m\t\tand more more text\u001b[0m",
"normal\x1b[43myellowbg\x1b[35mmagentafg\x1b[1mbold\x1b[49mdefaultbg\x1b[39mdefaultfg\x1b[22mnormal",
].join("\n");

View File

@ -9,6 +9,7 @@
"url": "https://github.com/jupyter/notebook.git"
},
"scripts": {
"lint": "eslint --quiet notebook/",
"bower": "bower install --allow-root --config.interactive=false",
"build:watch": "concurrent \"npm run build:css:watch\" \"npm run build:js:watch\"",
"build": "npm run build:css && npm run build:js",
@ -18,17 +19,21 @@
"build:js:watch": "npm run build:js -- --watch"
},
"devDependencies": {
"babel-cli": "^6.7.5",
"babel-core": "^6.7.4",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"bower": "*",
"concurrently": "^1.0.0",
"eslint": "^2.8.0",
"less": "~2",
"requirejs": "^2.1.17",
"underscore": "^1.8.3",
"webpack": "^1.12.13"
},
"dependencies": {
"moment": "^2.8.4"
"moment": "^2.8.4",
"preact": "^4.5.1",
"preact-compat": "^1.7.0"
}
}

View File

@ -22,7 +22,6 @@ from distutils.cmd import Command
from distutils.version import LooseVersion
from fnmatch import fnmatch
from glob import glob
from multiprocessing.pool import ThreadPool
from subprocess import check_call, check_output
if sys.platform == 'win32':

View File

@ -1,6 +1,12 @@
var _ = require('underscore');
var path = require('path');
var sourcemaps = 'source-map'
if(process.argv.indexOf('-w') !== -1 || process.argv.indexOf('-w') !== -1 ){
console.log('watch mode detected, will switch to cheep sourcemaps')
sourcemaps = 'eval-source-map';
}
var commonConfig = {
resolve: {
root: [
@ -44,7 +50,7 @@ function buildConfig(appName) {
filename: 'main.min.js',
path: './notebook/static/' + appName + '/js/built'
},
devtool: 'source-map',
devtool: sourcemaps,
});
}
@ -61,7 +67,7 @@ module.exports = [
path: './notebook/static/services/built',
libraryTarget: 'amd'
},
devtool: 'source-map',
devtool: sourcemaps,
}),
_.extend({}, commonConfig, {
entry: './notebook/static/index.js',
@ -70,6 +76,6 @@ module.exports = [
path: './notebook/static/built',
libraryTarget: 'amd'
},
devtool: 'source-map',
devtool: sourcemaps,
}),
].map(buildConfig);