Mailspring/src/keymap-manager.es6
2016-10-17 18:07:35 -07:00

205 lines
5.7 KiB
JavaScript

import fs from 'fs-plus'
import path from 'path'
import mousetrap from 'mousetrap'
import {ipcRenderer} from 'electron'
import {Emitter, Disposable} from 'event-kit'
let suspended = false
const templateConfigKey = 'core.keymapTemplate'
/*
By default, Mousetrap stops all hotkeys within text inputs. Override this to
more specifically block only hotkeys that have no modifier keys (things like
Gmail's "x", while allowing standard hotkeys.)
*/
mousetrap.prototype.stopCallback = (e, element, combo) => {
if (suspended) {
return true;
}
const withinTextInput = element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.isContentEditable
const withinWebview = element.tagName === 'WEBVIEW';
if (withinWebview) {
return true;
}
if (withinTextInput) {
const isPlainKey = !/(mod|command|ctrl)/.test(combo);
const isReservedTextEditingShortcut = /(mod|command|ctrl)\+(a|x|c|v)/.test(combo);
return isPlainKey || isReservedTextEditingShortcut;
}
return false;
}
class KeymapFile {
constructor(manager, filePath) {
this._manager = manager;
this._path = filePath;
this._bindings = {};
this._disposable = null;
}
load = () => {
let keymaps = null;
try {
keymaps = JSON.parse(fs.readFileSync(this._path))
} catch (e) {
if (e.code === 'ENOENT') {
return;
}
console.error(e);
return;
}
this._bindings = {};
Object.keys(keymaps).forEach((command) => {
let keystrokesArray = keymaps[command];
if (!(keystrokesArray instanceof Array)) {
keystrokesArray = [keystrokesArray];
}
for (const keystrokes of keystrokesArray) {
this._manager.ensureKeystrokesRegistered(keystrokes);
this._bindings[command] = this._bindings[command] || [];
this._bindings[command].push(keystrokes);
}
});
this._manager.keymapCacheInvalidated();
}
watch() {
fs.watch(this._path, this.load);
}
bindings() {
return this._bindings;
}
}
export default class KeymapManager {
constructor({configDirPath, resourcePath}) {
this.configDirPath = configDirPath;
this.resourcePath = resourcePath;
this._emitter = new Emitter();
this._registered = {};
this._files = [];
}
getUserKeymapPath() {
return path.join(this.configDirPath, 'keymap.json');
}
suspendAllKeymaps() {
NylasEnv.menu.sendToBrowserProcess(NylasEnv.menu.template, {});
suspended = true;
}
resumeAllKeymaps() {
NylasEnv.menu.update();
suspended = false;
}
loadKeymaps = () => {
// Load the base keymap and the base.platform keymap
this.loadKeymap(path.join(this.resourcePath, 'keymaps', 'base.json'))
this.loadKeymap(path.join(this.resourcePath, 'keymaps', `base-${process.platform}.json`))
// Load the template keymap (Gmail, Mail.app, etc.) the user has chosen
if (this._unobserveTemplate) {
this._unobserveTemplate.dispose();
}
this._unobserveTemplate = NylasEnv.config.observe(templateConfigKey, this.loadTemplateKeymap);
const userKeymapPath = this.getUserKeymapPath()
if (!fs.existsSync(userKeymapPath)) {
fs.writeFileSync(userKeymapPath, "{}");
}
this.userKeymap = new KeymapFile(this, userKeymapPath)
this.userKeymap.load()
this.userKeymap.watch()
}
loadTemplateKeymap = () => {
if (this._removeTemplate) {
this._removeTemplate.dispose();
}
let templateFile = NylasEnv.config.get(templateConfigKey);
if (templateFile) {
templateFile = templateFile.replace("GoogleInbox", "Inbox by Gmail");
const templateKeymapPath = path.join(this.resourcePath, 'keymaps', 'templates', `${templateFile}.json`);
this._removeTemplate = this.loadKeymap(templateKeymapPath);
}
}
loadKeymap(filePath) {
const file = new KeymapFile(this, filePath);
this._files.push(file);
file.load();
return new Disposable(() => {
this._files = this._files.filter(f => f !== file);
this.keymapCacheInvalidated();
});
}
ensureKeystrokesRegistered(keystrokes) {
if (this._registered[keystrokes]) {
return;
}
this._registered[keystrokes] = true;
mousetrap.bind(keystrokes, () => {
for (const command of (this._commandsCache[keystrokes] || [])) {
if (command.startsWith('application:')) {
ipcRenderer.send('command', command);
} else {
NylasEnv.commands.dispatch(command);
}
}
return false
});
}
keymapCacheInvalidated() {
this._bindingsCache = {};
for (const file of this._files) {
const fileBindings = file.bindings();
for (const command of Object.keys(fileBindings)) {
const keystrokesArray = fileBindings[command];
this._bindingsCache[command] = (this._bindingsCache[command] || []).concat(keystrokesArray);
}
}
if (this.userKeymap) {
const userBindings = this.userKeymap.bindings();
for (const command of Object.keys(userBindings)) {
this._bindingsCache[command] = userBindings[command];
}
}
this._commandsCache = {};
for (const command of Object.keys(this._bindingsCache)) {
for (const keystrokes of this._bindingsCache[command]) {
if (!this._commandsCache[keystrokes]) {
this._commandsCache[keystrokes] = [];
}
if (!this._commandsCache[keystrokes].includes(command)) {
this._commandsCache[keystrokes].push(command);
}
}
}
this._emitter.emit('on-did-reload-keymap');
}
onDidReloadKeymap = (callback) => {
return this._emitter.on('on-did-reload-keymap', callback);
}
getBindingsForAllCommands() {
return this._bindingsCache;
}
getBindingsForCommand(command) {
return this._bindingsCache[command] || [];
}
}