2016-05-06 08:05:51 +08:00
|
|
|
import React from 'react';
|
|
|
|
import ReactDOM from 'react-dom';
|
2017-09-27 02:33:08 +08:00
|
|
|
import PropTypes from 'prop-types';
|
2016-05-06 08:05:51 +08:00
|
|
|
import _ from 'underscore';
|
2017-09-27 02:46:00 +08:00
|
|
|
import { Flexbox } from 'mailspring-component-kit';
|
2018-10-08 13:19:30 +08:00
|
|
|
import { localized } from 'mailspring-exports';
|
2016-05-06 08:05:51 +08:00
|
|
|
import fs from 'fs';
|
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
import { keyAndModifiersForEvent } from './mousetrap-keybinding-helpers';
|
2016-05-06 08:05:51 +08:00
|
|
|
|
2019-03-05 03:03:12 +08:00
|
|
|
interface CommandKeybindingProps {
|
|
|
|
bindings: string[];
|
|
|
|
label: string;
|
|
|
|
command: string;
|
|
|
|
}
|
|
|
|
interface CommandKeybindingState {
|
|
|
|
editing: boolean;
|
|
|
|
editingBinding?: string;
|
|
|
|
modifiers?: string[];
|
|
|
|
keys?: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class CommandKeybinding extends React.Component<
|
|
|
|
CommandKeybindingProps,
|
|
|
|
CommandKeybindingState
|
|
|
|
> {
|
2016-05-06 08:05:51 +08:00
|
|
|
static propTypes = {
|
2017-09-27 02:33:08 +08:00
|
|
|
bindings: PropTypes.array,
|
|
|
|
label: PropTypes.string,
|
|
|
|
command: PropTypes.string,
|
|
|
|
};
|
2016-05-06 08:05:51 +08:00
|
|
|
|
2019-03-05 03:03:12 +08:00
|
|
|
_mounted = false;
|
|
|
|
|
2016-05-06 08:05:51 +08:00
|
|
|
constructor(props) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
editing: false,
|
2017-09-27 02:33:08 +08:00
|
|
|
};
|
2016-05-06 08:05:51 +08:00
|
|
|
}
|
2019-03-05 03:03:12 +08:00
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this._mounted = true;
|
|
|
|
}
|
|
|
|
|
2016-05-06 08:05:51 +08:00
|
|
|
componentDidUpdate() {
|
2017-09-27 02:33:08 +08:00
|
|
|
const { modifiers, keys, editing } = this.state;
|
2016-05-06 08:05:51 +08:00
|
|
|
if (editing) {
|
2017-09-27 02:33:08 +08:00
|
|
|
const finished = (modifiers.length > 0 && keys.length > 0) || keys.length >= 2;
|
2016-05-06 08:05:51 +08:00
|
|
|
if (finished) {
|
2019-03-05 03:03:12 +08:00
|
|
|
(ReactDOM.findDOMNode(this) as HTMLElement).blur();
|
2016-05-06 08:05:51 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-05 03:03:12 +08:00
|
|
|
componentWillUnmount() {
|
|
|
|
this._mounted = false;
|
|
|
|
}
|
|
|
|
|
2016-05-06 08:05:51 +08:00
|
|
|
_formatKeystrokes(original) {
|
|
|
|
// On Windows, display cmd-shift-c
|
2017-09-27 02:33:08 +08:00
|
|
|
if (process.platform === 'win32') return original;
|
2016-05-06 08:05:51 +08:00
|
|
|
|
|
|
|
// Replace "cmd" => ⌘, etc.
|
|
|
|
const modifiers = [
|
|
|
|
[/\+(?!$)/gi, ''],
|
|
|
|
[/command/gi, '⌘'],
|
|
|
|
[/meta/gi, '⌘'],
|
|
|
|
[/alt/gi, '⌥'],
|
|
|
|
[/shift/gi, '⇧'],
|
|
|
|
[/ctrl/gi, '^'],
|
2017-09-27 02:33:08 +08:00
|
|
|
[/mod/gi, process.platform === 'darwin' ? '⌘' : '^'],
|
2016-05-06 08:05:51 +08:00
|
|
|
];
|
|
|
|
let clean = original;
|
|
|
|
for (const [regexp, char] of modifiers) {
|
|
|
|
clean = clean.replace(regexp, char);
|
|
|
|
}
|
|
|
|
|
|
|
|
// ⌘⇧c => ⌘⇧C
|
|
|
|
if (clean !== original) {
|
|
|
|
clean = clean.toUpperCase();
|
|
|
|
}
|
|
|
|
|
|
|
|
// backspace => Backspace
|
|
|
|
if (original.length > 1 && clean === original) {
|
|
|
|
clean = clean[0].toUpperCase() + clean.slice(1);
|
|
|
|
}
|
|
|
|
return clean;
|
|
|
|
}
|
|
|
|
|
|
|
|
_renderKeystrokes = (keystrokes, idx) => {
|
|
|
|
const elements = [];
|
|
|
|
const splitKeystrokes = keystrokes.split(' ');
|
|
|
|
splitKeystrokes.forEach((keystroke, kidx) => {
|
|
|
|
elements.push(<span key={kidx}>{this._formatKeystrokes(keystroke)}</span>);
|
|
|
|
if (kidx < splitKeystrokes.length - 1) {
|
2017-09-27 02:33:08 +08:00
|
|
|
elements.push(
|
|
|
|
<span className="then" key={`then${kidx}`}>
|
2018-10-08 13:19:30 +08:00
|
|
|
{` ${localized('then')} `}
|
2017-09-27 02:33:08 +08:00
|
|
|
</span>
|
|
|
|
);
|
2016-05-06 08:05:51 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return (
|
2017-09-27 02:33:08 +08:00
|
|
|
<span key={`keystrokes-${idx}`} className="shortcut-value">
|
|
|
|
{elements}
|
|
|
|
</span>
|
2016-05-06 08:05:51 +08:00
|
|
|
);
|
2017-09-27 02:33:08 +08:00
|
|
|
};
|
2016-05-06 08:05:51 +08:00
|
|
|
|
|
|
|
_onEdit = () => {
|
2017-09-27 02:33:08 +08:00
|
|
|
this.setState({ editing: true, editingBinding: null, keys: [], modifiers: [] });
|
2017-09-27 02:36:58 +08:00
|
|
|
AppEnv.keymaps.suspendAllKeymaps();
|
2017-09-27 02:33:08 +08:00
|
|
|
};
|
2016-05-06 08:05:51 +08:00
|
|
|
|
|
|
|
_onFinishedEditing = () => {
|
|
|
|
if (this.state.editingBinding) {
|
2017-09-27 02:36:58 +08:00
|
|
|
const keymapPath = AppEnv.keymaps.getUserKeymapPath();
|
2016-05-16 01:19:03 +08:00
|
|
|
let keymaps = {};
|
2016-05-06 08:05:51 +08:00
|
|
|
|
|
|
|
try {
|
|
|
|
const exists = fs.existsSync(keymapPath);
|
2016-05-16 01:19:03 +08:00
|
|
|
if (exists) {
|
2019-03-05 03:03:12 +08:00
|
|
|
keymaps = JSON.parse(fs.readFileSync(keymapPath).toString());
|
2016-05-16 01:19:03 +08:00
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
console.error(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
keymaps[this.props.command] = this.state.editingBinding;
|
|
|
|
|
|
|
|
try {
|
2016-05-06 08:05:51 +08:00
|
|
|
fs.writeFileSync(keymapPath, JSON.stringify(keymaps, null, 2));
|
|
|
|
} catch (err) {
|
2017-09-27 02:36:58 +08:00
|
|
|
AppEnv.showErrorDialog(
|
2018-10-08 13:19:30 +08:00
|
|
|
localized(`Mailspring was unable to modify your keymaps at %@.`, keymapPath) +
|
|
|
|
' ' +
|
|
|
|
err.toString()
|
2017-09-27 02:33:08 +08:00
|
|
|
);
|
2016-05-06 08:05:51 +08:00
|
|
|
}
|
|
|
|
}
|
2019-03-05 03:03:12 +08:00
|
|
|
|
2017-09-27 02:36:58 +08:00
|
|
|
AppEnv.keymaps.resumeAllKeymaps();
|
2019-03-05 03:03:12 +08:00
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!this._mounted) return;
|
|
|
|
this.setState({ editing: false, editingBinding: null });
|
|
|
|
}, 100);
|
2017-09-27 02:33:08 +08:00
|
|
|
};
|
2016-05-06 08:05:51 +08:00
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
_onKey = event => {
|
2016-05-06 08:05:51 +08:00
|
|
|
if (!this.state.editing) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
|
|
const [eventKey, eventMods] = keyAndModifiersForEvent(event);
|
|
|
|
if (!eventKey || ['mod', 'meta', 'command', 'ctrl', 'alt', 'shift'].includes(eventKey)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
let { keys, modifiers } = this.state;
|
2016-05-06 08:05:51 +08:00
|
|
|
keys = keys.concat([eventKey]);
|
|
|
|
modifiers = _.uniq(modifiers.concat(eventMods));
|
|
|
|
|
|
|
|
let editingBinding = keys.join(' ');
|
|
|
|
if (modifiers.length > 0) {
|
2019-06-11 13:46:17 +08:00
|
|
|
editingBinding = [...modifiers, ...keys].join('+');
|
2016-05-06 08:05:51 +08:00
|
|
|
editingBinding = editingBinding.replace(/(meta|command|ctrl)/g, 'mod');
|
|
|
|
}
|
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
this.setState({ keys, modifiers, editingBinding });
|
|
|
|
};
|
2016-05-06 08:05:51 +08:00
|
|
|
|
|
|
|
render() {
|
2017-09-27 02:33:08 +08:00
|
|
|
const { editing, editingBinding } = this.state;
|
2016-05-06 08:05:51 +08:00
|
|
|
const bindings = editingBinding ? [editingBinding] : this.props.bindings;
|
|
|
|
|
2019-03-05 03:03:12 +08:00
|
|
|
let value: React.ReactChild | React.ReactChild[] = 'None';
|
2016-05-06 08:05:51 +08:00
|
|
|
if (bindings.length > 0) {
|
|
|
|
value = _.uniq(bindings).map(this._renderKeystrokes);
|
|
|
|
}
|
|
|
|
|
2017-09-27 02:33:08 +08:00
|
|
|
let classnames = 'shortcut';
|
2016-05-06 08:05:51 +08:00
|
|
|
if (editing) {
|
2017-09-27 02:33:08 +08:00
|
|
|
classnames += ' editing';
|
2016-05-06 08:05:51 +08:00
|
|
|
}
|
|
|
|
return (
|
|
|
|
<Flexbox
|
|
|
|
className={classnames}
|
|
|
|
tabIndex={-1}
|
|
|
|
onKeyDown={this._onKey}
|
|
|
|
onKeyPress={this._onKey}
|
|
|
|
onFocus={this._onEdit}
|
2016-05-07 07:24:40 +08:00
|
|
|
onBlur={this._onFinishedEditing}
|
|
|
|
>
|
2017-09-27 02:33:08 +08:00
|
|
|
<div className="col-left shortcut-name">{this.props.label}</div>
|
2016-05-06 08:05:51 +08:00
|
|
|
<div className="col-right">
|
|
|
|
<div className="values">{value}</div>
|
|
|
|
</div>
|
|
|
|
</Flexbox>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|