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

[POC] Shortcut editor (for command mode) #1265

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .babelrc
@@ -0,0 +1 @@
{ "presets": ["es2015"] }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll probably want react in your presets here, especially if you use JSX. I'm fine either way and won't bikeshed on the myriad alternatives.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't use JSX for now. I sticked to doing things manually.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, cool!

14 changes: 14 additions & 0 deletions .eslintrc.json
@@ -0,0 +1,14 @@
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"semi": 2,
"comma-dangle": 2,
"no-unreachable" : 2
}
}
28 changes: 25 additions & 3 deletions notebook/static/base/js/keyboard.js
Expand Up @@ -10,7 +10,7 @@

define([
'base/js/utils',
'underscore',
'underscore'
], function(utils, _) {
"use strict";

Expand Down Expand Up @@ -42,7 +42,7 @@ define([
'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,
'insert': 45, 'delete': 46, 'numlock': 144
};

// These apply to Firefox and Opera
Expand Down Expand Up @@ -334,6 +334,28 @@ define([
}
};

ShortcutManager.prototype.is_available_shortcut = function(shortcut){
const shortcut_array = shortcut.split(',');
return this._is_available_shortcut(shortcut_array, this._shortcuts);
};

ShortcutManager.prototype._is_available_shortcut = function(shortcut_array, tree){
console.info('testing for ', shortcut_array, '...');
var current_node = tree[shortcut_array[0]];
if(!shortcut_array[0]){
return false;
}
if(current_node === undefined){
return true;
} else {
if (typeof(current_node) == 'string'){
return false;
} else { // assume is a sub-shortcut tree
return this._is_available_shortcut(shortcut_array.slice(1), current_node);
}
}
};

ShortcutManager.prototype._set_leaf = function(shortcut_array, action_name, tree){
var current_node = tree[shortcut_array[0]];
if(shortcut_array.length === 1){
Expand Down Expand Up @@ -470,7 +492,7 @@ define([
normalize_key : normalize_key,
normalize_shortcut : normalize_shortcut,
shortcut_to_event : shortcut_to_event,
event_to_shortcut : event_to_shortcut,
event_to_shortcut : event_to_shortcut
};

return keyboard;
Expand Down
16 changes: 11 additions & 5 deletions notebook/static/notebook/js/actions.js
Expand Up @@ -62,11 +62,17 @@ 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) {
env.notebook.restart_kernel({confirm: false});
},
}
},
'confirm-restart-kernel':{
icon: 'fa-repeat',
Expand Down Expand Up @@ -479,7 +485,7 @@ define(function(require){
env.pager.collapse();
}
}
},
}
};

/**
Expand Down Expand Up @@ -543,15 +549,15 @@ define(function(require){
event.preventDefault();
}
return env.notebook.scroll_manager.scroll(1);
},
}
},
'scroll-notebook-up': {
handler: function(env, event) {
if(event){
event.preventDefault();
}
return env.notebook.scroll_manager.scroll(-1);
},
}
},
'scroll-cell-center': {
help: "Scroll the current cell to the center",
Expand Down Expand Up @@ -621,7 +627,7 @@ define(function(require){
}
return false;
}
},
}
};

// private stuff that prepend `jupyter-notebook:` to actions names
Expand Down
8 changes: 4 additions & 4 deletions notebook/static/notebook/js/keyboardmanager.js
Expand Up @@ -10,7 +10,7 @@

define([
'base/js/utils',
'base/js/keyboard',
'base/js/keyboard'
], function(utils, keyboard) {
"use strict";

Expand Down Expand Up @@ -106,7 +106,7 @@ define([
'ctrl-enter' : 'jupyter-notebook:run-cell',
'alt-enter' : 'jupyter-notebook:run-cell-and-insert-below',
// cmd on mac, ctrl otherwise
'cmdtrl-s' : 'jupyter-notebook:save-notebook',
'cmdtrl-s' : 'jupyter-notebook:save-notebook'
};
};

Expand All @@ -117,7 +117,7 @@ define([
'ctrl-m' : 'jupyter-notebook:enter-command-mode',
'up' : 'jupyter-notebook:move-cursor-up',
'down' : 'jupyter-notebook:move-cursor-down',
'ctrl-shift--' : 'jupyter-notebook:split-cell-at-cursor',
'ctrl-shift--' : 'jupyter-notebook:split-cell-at-cursor'
};
};

Expand Down Expand Up @@ -161,7 +161,7 @@ define([
'l' : 'jupyter-notebook:toggle-cell-line-numbers',
'h' : 'jupyter-notebook:show-keyboard-shortcuts',
'z' : 'jupyter-notebook:undo-cell-deletion',
'q' : 'jupyter-notebook:close-pager',
'q' : 'jupyter-notebook:close-pager'
};
};

Expand Down
5 changes: 5 additions & 0 deletions notebook/static/notebook/js/notebook.js
Expand Up @@ -29,6 +29,7 @@ define(function (require) {
var attachments_celltoolbar = require('notebook/js/celltoolbarpresets/attachments');
var scrollmanager = require('notebook/js/scrollmanager');
var commandpalette = require('notebook/js/commandpalette');
var shortcuts_editor = require('notebook/js/shortcuts_editor');

var _SOFT_SELECTION_CLASS = 'jupyter-soft-selected';

Expand Down Expand Up @@ -361,6 +362,10 @@ define(function (require) {
var x = new commandpalette.CommandPalette(this);
};

Notebook.prototype.show_shortcuts_editor = function() {
var x = new shortcuts_editor.ShortcutEditor(this);
};

/**
* Trigger a warning dialog about missing functionality from newer minor versions
*/
Expand Down
18 changes: 14 additions & 4 deletions notebook/static/notebook/js/quickhelp.js
Expand Up @@ -153,13 +153,22 @@ define([
return hum;
}

function humanize_shortcut(shortcut){
function _humanize_sequence(sequence){
var joinchar = ',';
var hum = _.map(sequence.replace(/meta/g, 'cmd').split(','), _humanize_shortcut).join(joinchar);
return hum;
}

function _humanize_shortcut(shortcut){
var joinchar = '-';
if (platform === 'MacOS'){
joinchar = '';
}
var sh = _.map(shortcut.split('-'), humanize_key ).join(joinchar);
return '<kbd>'+sh+'</kbd>';
return _.map(shortcut.split('-'), humanize_key ).join(joinchar);
}

function humanize_shortcut(shortcut){
return '<kbd>'+_humanize_shortcut(shortcut)+'</kbd>';
}


Expand Down Expand Up @@ -301,6 +310,7 @@ define([

return {'QuickHelp': QuickHelp,
humanize_shortcut: humanize_shortcut,
humanize_sequence: humanize_sequence
humanize_sequence: humanize_sequence,
_humanize_sequence: _humanize_sequence,
};
});
143 changes: 143 additions & 0 deletions notebook/static/notebook/js/shortcuts_editor.js
@@ -0,0 +1,143 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

define(function(require){
"use strict";

const QH = require("notebook/js/quickhelp");
const dialog = require("base/js/dialog");
const React = require("React");
const ReactDom = require("react-dom");

/**
* Humanize the action name to be consumed by user.
* internaly the actions anem are of the form
* <namespace>:<description-with-dashes>
* we drop <namesapce> 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 = React.createClass({
displayName: 'KeyBindings',
getInitialState: function() {
return {shrt:''};
},
handleShrtChange: function (element){
this.setState({shrt:element.target.value});
},
render: function(){
const that = this;
const av = this.props.available(this.state.shrt);
return React.createElement('div',{style:{borderBottom: '1px solid gray'}, className:'jupyter-keybindings'},
this.props.shortcut?
React.createElement('i', {className: "pull-right fa fa-times", alt: 'remove title'+this.props.shortcut,
onClick:()=>{
console.log('will unbind ::', this.props.shortcuts, this.props);
that.props.unbind(this.props.shortcut);
}}):
React.createElement('i', {className: "pull-right fa fa-plus", alt: 'add-keyboard-shortcut',
onClick:()=>{
av?that.props.onAddBindings(that.state.shrt, that.props.ckey):undefined;
}
}),
this.props.shortcut? undefined :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you don't want an element to show, return null.

React.createElement('input', {type:'text', placeholder:'add shortcut',
className:'pull-right'+(av?'':' alert alert-danger'), value:this.state.shrt, onChange:this.handleShrtChange
}),
this.props.shortcut? React.createElement('span', {className: 'pull-right'}, React.createElement('kbd', {}, this.props.shortcut)): undefined,
React.createElement('div', {title: '(' +this.props.ckey + ')' , className:'jupyter-keybindings-text'}, this.props.display )
);
}
});

const KeyBindingList = React.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 React.createElement(KeyBinding, Object.assign({}, binding, {onAddBindings:(shortcut, action)=>{
this.props.bind(shortcut, action);
this.setState({data:this.props.callback()});
},
available:this.props.available,
unbind: this.props.unbind
}));
});

return React.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 shortcut = notebook.keyboard_manager.command_shortcuts.get_action_shortcut(action_id) ||
notebook.keyboard_manager.edit_shortcuts.get_action_shortcut(action_id);
if (shortcut) {
shortcut = QH._humanize_sequence(shortcut);
}

src.push({
display: humanize_action_id(action_id),
shortcut: shortcut,
key:action_id, // react specific thing
ckey: action_id
});
}

return src;
};


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');
ReactDom.render(
React.createElement(KeyBindingList, {
callback:()=>{ return get_shortcuts_data(notebook);},
bind: (shortcut, command)=>{return notebook.keyboard_manager.command_shortcuts.add_shortcut(shortcut, command);},
unbind: (shortcut) => { return notebook.keyboard_manager.command_shortcuts.remove_shortcut(shortcut);},
available: (shrt) => { return notebook.keyboard_manager.command_shortcuts.is_available_shortcut(shrt);}
}),
body.get(0)
);
};
return {'ShortcutEditor': ShortcutEditor};
});
15 changes: 15 additions & 0 deletions notebook/static/notebook/less/notebook.less
Expand Up @@ -99,3 +99,18 @@ kbd {
padding-top: 1px;
padding-bottom: 1px;
}

.jupyter-keybindings {
padding: 0px;
line-height: 24px;
}

.jupyter-keybindings input {
margin: 0;
padding: 0;
border: none;
}

.jupyter-keybindings i {
padding: 6px;
}