Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a shortcut editor for the notebook. #1347

Merged
merged 3 commits into from
May 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"presets": ["es2015"],
}
5 changes: 5 additions & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.min.js
*components*
*node_modules*
*built*
*build*
13 changes: 13 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -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
}
}
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
35 changes: 24 additions & 11 deletions notebook/static/base/js/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
};
Expand Down
6 changes: 6 additions & 0 deletions notebook/static/notebook/js/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 19 additions & 0 deletions notebook/static/notebook/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
58 changes: 30 additions & 28 deletions notebook/static/notebook/js/notebook.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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');
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -3160,5 +3164,3 @@ define(function (require) {
this.load_notebook(this.notebook_path);
};

return {'Notebook': Notebook};
});
173 changes: 173 additions & 0 deletions notebook/static/notebook/js/shortcuteditor.js
Original file line number Diff line number Diff line change
@@ -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)
);
};
Loading