1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Desktop: Added KeymapService to manage keyboard shortcuts (#3252)

This commit is contained in:
Anjula Karunarathne 2020-08-02 16:56:55 +05:30 committed by GitHub
parent cc8c200826
commit 88f22fabf7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 622 additions and 40 deletions

View File

@ -155,6 +155,7 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
ReactNativeClient/lib/services/KeymapService.js
ReactNativeClient/lib/services/ResourceEditWatcher/index.js
ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js
ReactNativeClient/lib/services/rest/actionApi.desktop.js

1
.gitignore vendored
View File

@ -146,6 +146,7 @@ ReactNativeClient/lib/services/keychain/KeychainServiceDriver.dummy.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.mobile.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriver.node.js
ReactNativeClient/lib/services/keychain/KeychainServiceDriverBase.js
ReactNativeClient/lib/services/KeymapService.js
ReactNativeClient/lib/services/ResourceEditWatcher/index.js
ReactNativeClient/lib/services/ResourceEditWatcher/reducer.js
ReactNativeClient/lib/services/rest/actionApi.desktop.js

View File

@ -0,0 +1,319 @@
require('app-module-path').addPath(__dirname);
const KeymapService = require('lib/services/KeymapService').default;
const keymapService = KeymapService.instance();
describe('services_KeymapService', () => {
describe('validateAccelerator', () => {
it('should identify valid Accelerators', () => {
const testCases = {
darwin: [
'F4',
'Cmd+F9',
'Option+Z',
'Option+Shift+F',
'Ctrl+Option+U',
'Option+Shift+Cmd+F9',
'Ctrl+Shift+Z',
'Option+Shift+Cmd+B',
],
linux: [
'F4',
'Ctrl+F9',
'Alt+Shift+F',
'Shift+U',
'Ctrl+Shift+T',
'Ctrl+Alt+Shift+Z',
'Alt+E',
'Alt+Shift+F9',
],
};
Object.entries(testCases).forEach(([platform, accelerators]) => {
keymapService.initialize(platform);
accelerators.forEach(accelerator => {
expect(() => keymapService.validateAccelerator(accelerator)).not.toThrow();
});
});
});
it('should identify invalid Accelerators', () => {
const testCases = {
darwin: [
'',
'A+Z',
'Cmd',
'Ctrl+',
'Option+Cmd',
'Ctrl+Shift',
'Cmd+H+A',
'Option+Shift+Cmd+J+O+P',
'Opptionn+F9',
'Ctrl+Shiftt+X',
'Cmd+Option+Shoft+T',
],
win32: [
'+',
'B+F4',
'Ctrl+',
'Ctrl+Shift+',
'Cmd+Alt',
'Ctrl+Shift+Alt',
'Cmd+H+A',
'Ctrl+Shift+Alt+J+O+P',
'Contrl+F9',
'Controller+Shift+X',
'Cmd+Option+Shoft+T',
],
};
Object.entries(testCases).forEach(([platform, accelerators]) => {
keymapService.initialize(platform);
accelerators.forEach(accelerator => {
expect(() => keymapService.validateAccelerator(accelerator)).toThrow();
});
});
});
});
describe('getAccelerator', () => {
beforeEach(() => keymapService.initialize());
it('should return the platform-specific default Accelerator', () => {
keymapService.initialize('darwin');
expect(keymapService.getAccelerator('newNote')).toEqual('Cmd+N');
expect(keymapService.getAccelerator('synchronize')).toEqual('Cmd+S');
expect(keymapService.getAccelerator('textSelectAll')).toEqual('Cmd+A');
expect(keymapService.getAccelerator('textBold')).toEqual('Cmd+B');
keymapService.initialize('linux');
expect(keymapService.getAccelerator('newNote')).toEqual('Ctrl+N');
expect(keymapService.getAccelerator('synchronize')).toEqual('Ctrl+S');
keymapService.initialize('win32');
expect(keymapService.getAccelerator('textSelectAll')).toEqual('Ctrl+A');
expect(keymapService.getAccelerator('textBold')).toEqual('Ctrl+B');
});
if ('should throw when an invalid command is requested', () => {
expect(() => keymapService.getAccelerator('totallyNonExistentCommand')).toThrow();
});
});
describe('setAccelerator', () => {
beforeEach(() => keymapService.initialize());
it('should update the Accelerator', () => {
keymapService.initialize('darwin');
const testCases_Darwin = [
{ command: 'newNote', accelerator: 'Ctrl+Option+Shift+N' },
{ command: 'synchronize', accelerator: 'F11' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Option+Cmd+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Option+Shift+Cmd+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Option+Shift+Cmd+B' },
];
testCases_Darwin.forEach(({ command, accelerator }) => {
keymapService.setAccelerator(command, accelerator);
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
keymapService.initialize('linux');
const testCases_Linux = [
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
{ command: 'synchronize', accelerator: 'F15' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
];
testCases_Linux.forEach(({ command, accelerator }) => {
keymapService.setAccelerator(command, accelerator);
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
});
});
describe('resetAccelerator', () => {
beforeEach(() => keymapService.initialize());
it('should reset the Accelerator', () => {
const testCases = [
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
{ command: 'synchronize', accelerator: null /* Disabled */ },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
];
testCases.forEach(({ command, accelerator }) => {
// Remember the default Accelerator value
const prevAccelerator = keymapService.getAccelerator(command);
// Update the Accelerator,
keymapService.setAccelerator(command, accelerator);
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
// and then reset it..
keymapService.resetAccelerator(command);
expect(keymapService.getAccelerator(command)).toEqual(prevAccelerator);
});
});
});
describe('setKeymap', () => {
beforeEach(() => keymapService.initialize());
it('should update the keymap', () => {
keymapService.initialize('darwin');
const customKeymap_Darwin = [
{ command: 'newNote', accelerator: 'Option+Shift+Cmd+N' },
{ command: 'synchronize', accelerator: 'F11' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Option+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Option+P' },
{ command: 'help', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Option+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Option+Shift+Cmd+B' },
{ command: 'focusElementSideBar', accelerator: 'Shift+Cmd+L' /* Default of focusElementNoteList */ },
{ command: 'focusElementNoteList', accelerator: 'Shift+Cmd+S' /* Default of focusElementSideBar */ },
];
expect(() => keymapService.setKeymap(customKeymap_Darwin)).not.toThrow();
customKeymap_Darwin.forEach(({ command, accelerator }) => {
// Also check if keymap is updated or not
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
keymapService.initialize('win32');
const customKeymap_Win32 = [
{ command: 'newNote', accelerator: 'Ctrl+Alt+Shift+N' },
{ command: 'synchronize', accelerator: 'F11' },
{ command: 'textBold', accelerator: 'Shift+F5' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'help', accelerator: null /* Disabled */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+T' },
{ command: 'focusElementNoteBody', accelerator: 'Ctrl+Alt+Shift+B' },
{ command: 'focusElementSideBar', accelerator: 'Ctrl+Shift+L' /* Default of focusElementNoteList */ },
{ command: 'focusElementNoteList', accelerator: 'Ctrl+Shift+S' /* Default of focusElementSideBar */ },
];
expect(() => keymapService.setKeymap(customKeymap_Win32)).not.toThrow();
customKeymap_Win32.forEach(({ command, accelerator }) => {
// Also check if keymap is updated or not
expect(keymapService.getAccelerator(command)).toEqual(accelerator);
});
});
it('should throw when the required properties are missing', () => {
const customKeymaps = [
[
{ commmmmand: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'print', accelerator: 'Alt+P' },
],
[
{ accelerator: 'Alt+P' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
],
[
{ command: 'showLocalSearch', accel: 'Ctrl+Alt+S' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
],
[
{ command: 'print' },
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
],
];
for (let i = 0; i < customKeymaps.length; i++) {
const customKeymap = customKeymaps[i];
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
}
});
it('should throw when the provided Accelerators are invalid', () => {
// Only one test case is provided since KeymapService.validateAccelerator() is already tested
const customKeymap = [
{ command: 'gotoAnything', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J+O+P+L+I+N' },
];
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
});
it('should throw when the provided commands are invalid', () => {
const customKeymap = [
{ command: 'totallyInvalidCommand', accelerator: 'Ctrl+Shift+G' },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' },
];
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
});
it('should throw when duplicate accelerators are provided', () => {
const customKeymaps_Darwin = [
[
{ command: 'showLocalSearch', accelerator: 'Option+Cmd+S' /* Duplicate */ },
{ command: 'gotoAnything', accelerator: 'Option+Cmd+S' /* Duplicate */ },
{ command: 'print', accelerator: 'Option+P' },
],
[
{ command: 'showLocalSearch', accelerator: 'Option+Cmd+S' },
{ command: 'print', accelerator: 'Cmd+G' /* Default of gotoAnything */ },
{ command: 'focusElementNoteTitle', accelerator: 'Option+Shift+Cmd+J' },
],
];
for (let i = 0; i < customKeymaps_Darwin.length; i++) {
const customKeymap = customKeymaps_Darwin[i];
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
// All items should be reset to default values
for (let j = 0; j < customKeymap.length; j++) {
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
}
}
const customKeymaps_Linux = [
[
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' /* Duplicate */ },
{ command: 'print', accelerator: 'Alt+P' },
{ command: 'gotoAnything', accelerator: 'Ctrl+Alt+S' /* Duplicate */ },
],
[
{ command: 'showLocalSearch', accelerator: 'Ctrl+Alt+S' },
{ command: 'print', accelerator: 'Ctrl+G' /* Default of gotoAnything */ },
{ command: 'focusElementNoteTitle', accelerator: 'Ctrl+Alt+Shift+J' },
],
];
for (let i = 0; i < customKeymaps_Linux.length; i++) {
const customKeymap = customKeymaps_Linux[i];
const defaultAccelerators = customKeymap.map(({ command }) => keymapService.getAccelerator(command));
expect(() => keymapService.setKeymap(customKeymap)).toThrow();
for (let j = 0; j < customKeymap.length; j++) {
expect(keymapService.getAccelerator(customKeymap[j].command)).toEqual(defaultAccelerators[j]);
}
}
});
});
});

View File

@ -30,6 +30,7 @@ const PluginManager = require('lib/services/PluginManager');
const RevisionService = require('lib/services/RevisionService');
const MigrationService = require('lib/services/MigrationService');
const CommandService = require('lib/services/CommandService').default;
const KeymapService = require('lib/services/KeymapService.js').default;
const TemplateUtils = require('lib/TemplateUtils');
const CssUtils = require('lib/CssUtils');
const resourceEditWatcherReducer = require('lib/services/ResourceEditWatcher/reducer').default;
@ -375,6 +376,7 @@ class Application extends BaseApplication {
if (this.lastMenuScreen_ === screen) return;
const cmdService = CommandService.instance();
const keymapService = KeymapService.instance();
const sortNoteFolderItems = (type) => {
const sortItems = [];
@ -413,10 +415,10 @@ class Application extends BaseApplication {
const sortFolderItems = sortNoteFolderItems('folders');
const focusItems = [
cmdService.commandToMenuItem('focusElementSideBar', 'CommandOrControl+Shift+S'),
cmdService.commandToMenuItem('focusElementNoteList', 'CommandOrControl+Shift+L'),
cmdService.commandToMenuItem('focusElementNoteTitle', 'CommandOrControl+Shift+N'),
cmdService.commandToMenuItem('focusElementNoteBody', 'CommandOrControl+Shift+B'),
cmdService.commandToMenuItem('focusElementSideBar'),
cmdService.commandToMenuItem('focusElementNoteList'),
cmdService.commandToMenuItem('focusElementNoteTitle'),
cmdService.commandToMenuItem('focusElementNoteBody'),
];
let toolsItems = [];
@ -471,9 +473,9 @@ class Application extends BaseApplication {
modulePath: module.path,
onError: console.warn,
destinationFolderId:
!module.isNoteArchive && moduleSource === 'file'
? selectedFolderId
: null,
!module.isNoteArchive && moduleSource === 'file'
? selectedFolderId
: null,
};
const service = new InteropService();
@ -512,8 +514,8 @@ class Application extends BaseApplication {
},
};
const newNoteItem = cmdService.commandToMenuItem('newNote', 'CommandOrControl+N');
const newTodoItem = cmdService.commandToMenuItem('newTodo', 'CommandOrControl+T');
const newNoteItem = cmdService.commandToMenuItem('newNote');
const newTodoItem = cmdService.commandToMenuItem('newTodo');
const newNotebookItem = cmdService.commandToMenuItem('newNotebook');
const printItem = cmdService.commandToMenuItem('print');
@ -539,7 +541,7 @@ class Application extends BaseApplication {
}, {
label: _('Insert template'),
visible: templateDirExists,
accelerator: 'CommandOrControl+Alt+I',
accelerator: keymapService.getAccelerator('insertTemplate'),
click: () => {
cmdService.execute('selectTemplate');
},
@ -566,7 +568,7 @@ class Application extends BaseApplication {
const toolsItemsWindowsLinux = toolsItemsFirst.concat([{
label: _('Options'),
visible: !shim.isMac(),
accelerator: 'CommandOrControl+,',
accelerator: shim.isMac() ? null : keymapService.getAccelerator('config'),
click: () => {
this.dispatch({
type: 'NAV_GO',
@ -648,7 +650,7 @@ class Application extends BaseApplication {
}, {
label: _('Preferences...'),
visible: shim.isMac() ? true : false,
accelerator: 'CommandOrControl+,',
accelerator: shim.isMac() ? keymapService.getAccelerator('config') : null,
click: () => {
this.dispatch({
type: 'NAV_GO',
@ -687,7 +689,7 @@ class Application extends BaseApplication {
type: 'separator',
},
cmdService.commandToMenuItem('synchronize', 'CommandOrControl+S'),
cmdService.commandToMenuItem('synchronize'),
shim.isMac() ? syncStatusItem : noItem, {
type: 'separator',
@ -697,13 +699,13 @@ class Application extends BaseApplication {
}, {
label: _('Hide %s', 'Joplin'),
platforms: ['darwin'],
accelerator: 'CommandOrControl+H',
accelerator: shim.isMac() ? keymapService.getAccelerator('hideApp') : null,
click: () => { bridge().electronApp().hide(); },
}, {
type: 'separator',
}, {
label: _('Quit'),
accelerator: 'CommandOrControl+Q',
accelerator: keymapService.getAccelerator('quit'),
click: () => { bridge().electronApp().quit(); },
}],
};
@ -717,9 +719,9 @@ class Application extends BaseApplication {
newNotebookItem, {
label: _('Close Window'),
platforms: ['darwin'],
accelerator: 'Command+W',
accelerator: shim.isMac() ? keymapService.getAccelerator('closeWindow') : null,
selector: 'performClose:',
}, {
}, {
type: 'separator',
}, {
label: _('Templates'),
@ -762,28 +764,28 @@ class Application extends BaseApplication {
id: 'edit',
label: _('&Edit'),
submenu: [
cmdService.commandToMenuItem('textCopy', 'CommandOrControl+C'),
cmdService.commandToMenuItem('textCut', 'CommandOrControl+X'),
cmdService.commandToMenuItem('textPaste', 'CommandOrControl+V'),
cmdService.commandToMenuItem('textSelectAll', 'CommandOrControl+A'),
cmdService.commandToMenuItem('textCopy'),
cmdService.commandToMenuItem('textCut'),
cmdService.commandToMenuItem('textPaste'),
cmdService.commandToMenuItem('textSelectAll'),
separator(),
cmdService.commandToMenuItem('textBold', 'CommandOrControl+B'),
cmdService.commandToMenuItem('textItalic', 'CommandOrControl+I'),
cmdService.commandToMenuItem('textLink', 'CommandOrControl+K'),
cmdService.commandToMenuItem('textCode', 'CommandOrControl+`'),
cmdService.commandToMenuItem('textBold'),
cmdService.commandToMenuItem('textItalic'),
cmdService.commandToMenuItem('textLink'),
cmdService.commandToMenuItem('textCode'),
separator(),
cmdService.commandToMenuItem('insertDateTime', 'CommandOrControl+Shift+T'),
cmdService.commandToMenuItem('insertDateTime'),
separator(),
cmdService.commandToMenuItem('focusSearch', shim.isMac() ? 'Shift+Command+F' : 'F6'),
cmdService.commandToMenuItem('showLocalSearch', 'CommandOrControl+F'),
cmdService.commandToMenuItem('focusSearch'),
cmdService.commandToMenuItem('showLocalSearch'),
],
},
view: {
label: _('&View'),
submenu: [
CommandService.instance().commandToMenuItem('toggleSidebar', shim.isMac() ? 'Option+Command+S' : 'F10'),
CommandService.instance().commandToMenuItem('toggleSidebar'),
CommandService.instance().commandToMenuItem('toggleNoteList'),
CommandService.instance().commandToMenuItem('toggleVisiblePanes', 'CommandOrControl+L'),
CommandService.instance().commandToMenuItem('toggleVisiblePanes'),
{
label: _('Layout button sequence'),
screens: ['Main'],
@ -865,9 +867,8 @@ class Application extends BaseApplication {
note: {
label: _('&Note'),
submenu: [
CommandService.instance().commandToMenuItem('startExternalEditing', 'CommandOrControl+E'),
CommandService.instance().commandToMenuItem('setTags', 'CommandOrControl+Alt+T'),
CommandService.instance().commandToMenuItem('startExternalEditing'),
CommandService.instance().commandToMenuItem('setTags'),
separator(),
CommandService.instance().commandToMenuItem('showNoteContentProperties'),
],
@ -881,7 +882,7 @@ class Application extends BaseApplication {
role: 'help', // Makes it add the "Search" field on macOS
submenu: [{
label: _('Website and documentation'),
accelerator: 'F1',
accelerator: keymapService.getAccelerator('help'),
click() { bridge().openExternal('https://joplinapp.org'); },
}, {
label: _('Joplin Forum'),
@ -1124,11 +1125,20 @@ class Application extends BaseApplication {
argv = await super.start(argv);
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
const dir = Setting.value('profileDir');
// Loads app-wide styles. (Markdown preview-specific styles loaded in app.js)
const filename = Setting.custom_css_files.JOPLIN_APP;
await CssUtils.injectCustomStyles(`${dir}/${filename}`);
const keymapService = KeymapService.instance();
try {
await KeymapService.instance().loadKeymap(`${dir}/keymap-desktop.json`);
} catch (err) {
bridge().showErrorMessageBox(err.message);
}
AlarmService.setDriver(new AlarmServiceDriverNode({ appName: packageInfo.build.appId }));
AlarmService.setLogger(reg.logger());
@ -1144,7 +1154,7 @@ class Application extends BaseApplication {
this.initRedux();
CommandService.instance().initialize(this.store());
CommandService.instance().initialize(this.store(), keymapService);
for (const command of commands) {
CommandService.instance().registerDeclaration(command.declaration);

View File

@ -14,6 +14,7 @@ const { surroundKeywords, nextWhitespaceIndex, removeDiacritics } = require('lib
const { mergeOverlappingIntervals } = require('lib/ArrayUtils.js');
const PLUGIN_NAME = 'gotoAnything';
const markupLanguageUtils = require('lib/markupLanguageUtils');
const KeymapService = require('lib/services/KeymapService.js').default;
class GotoAnything {
@ -467,7 +468,7 @@ GotoAnything.manifest = {
name: 'main',
parent: 'tools',
label: _('Goto Anything...'),
accelerator: 'CommandOrControl+G',
accelerator: () => KeymapService.instance().getAccelerator('gotoAnything'),
screens: ['Main'],
},
],

View File

@ -1,3 +1,4 @@
import KeymapService from './KeymapService';
const BaseService = require('lib/services/BaseService');
const eventManager = require('lib/eventManager');
@ -75,8 +76,11 @@ export default class CommandService extends BaseService {
private commandPreviousStates_:CommandStates = {};
private mapStateToPropsIID_:any = null;
initialize(store:any) {
private keymapService:KeymapService = null;
initialize(store:any, keymapService:KeymapService) {
utils.store = store;
this.keymapService = keymapService;
}
public on(eventName:string, callback:Function) {
@ -265,7 +269,7 @@ export default class CommandService extends BaseService {
};
}
commandToMenuItem(commandName:string, accelerator:string = null, executeArgs:any = null) {
commandToMenuItem(commandName:string, executeArgs:any = null) {
const command = this.commandByName(commandName);
const item:any = {
@ -276,8 +280,10 @@ export default class CommandService extends BaseService {
},
};
if (accelerator) item.accelerator = accelerator;
if (command.declaration.role) item.role = command.declaration.role;
if (this.keymapService.hasAccelerator(commandName)) {
item.accelerator = this.keymapService.getAccelerator(commandName);
}
return item;
}

View File

@ -0,0 +1,240 @@
const fs = require('fs-extra');
const BaseService = require('lib/services/BaseService');
const { shim } = require('lib/shim.js');
const keysRegExp = /^([0-9A-Z)!@#$%^&*(:+<_>?~{|}";=,\-./`[\\\]']|F1*[1-9]|F10|F2[0-4]|Plus|Space|Tab|Backspace|Delete|Insert|Return|Enter|Up|Down|Left|Right|Home|End|PageUp|PageDown|Escape|Esc|VolumeUp|VolumeDown|VolumeMute|MediaNextTrack|MediaPreviousTrack|MediaStop|MediaPlayPause|PrintScreen)$/;
const modifiersRegExp = {
darwin: /^(Ctrl|Option|Shift|Cmd)$/,
default: /^(Ctrl|Alt|AltGr|Shift|Super)$/,
};
const defaultKeymap = {
darwin: [
{ accelerator: 'Cmd+N', command: 'newNote' },
{ accelerator: 'Cmd+T', command: 'newTodo' },
{ accelerator: 'Cmd+S', command: 'synchronize' },
{ accelerator: 'Cmd+P', command: 'print' },
{ accelerator: 'Cmd+H', command: 'hideApp' },
{ accelerator: 'Cmd+Q', command: 'quit' },
{ accelerator: 'Cmd+,', command: 'config' },
{ accelerator: 'Cmd+W', command: 'closeWindow' },
{ accelerator: 'Option+Cmd+I', command: 'insertTemplate' },
{ accelerator: 'Cmd+C', command: 'textCopy' },
{ accelerator: 'Cmd+X', command: 'textCut' },
{ accelerator: 'Cmd+V', command: 'textPaste' },
{ accelerator: 'Cmd+A', command: 'textSelectAll' },
{ accelerator: 'Cmd+B', command: 'textBold' },
{ accelerator: 'Cmd+I', command: 'textItalic' },
{ accelerator: 'Cmd+K', command: 'textLink' },
{ accelerator: 'Cmd+`', command: 'textCode' },
{ accelerator: 'Shift+Cmd+T', command: 'insertDateTime' },
{ accelerator: 'Shift+Cmd+F', command: 'focusSearch' },
{ accelerator: 'Cmd+F', command: 'showLocalSearch' },
{ accelerator: 'Shift+Cmd+S', command: 'focusElementSideBar' },
{ accelerator: 'Shift+Cmd+L', command: 'focusElementNoteList' },
{ accelerator: 'Shift+Cmd+N', command: 'focusElementNoteTitle' },
{ accelerator: 'Shift+Cmd+B', command: 'focusElementNoteBody' },
{ accelerator: 'Option+Cmd+S', command: 'toggleSidebar' },
{ accelerator: 'Cmd+L', command: 'toggleVisiblePanes' },
{ accelerator: 'Cmd+0', command: 'zoomActualSize' },
{ accelerator: 'Cmd+E', command: 'startExternalEditing' },
{ accelerator: 'Option+Cmd+T', command: 'setTags' },
{ accelerator: 'Cmd+G', command: 'gotoAnything' },
{ accelerator: 'F1', command: 'help' },
],
default: [
{ accelerator: 'Ctrl+N', command: 'newNote' },
{ accelerator: 'Ctrl+T', command: 'newTodo' },
{ accelerator: 'Ctrl+S', command: 'synchronize' },
{ accelerator: 'Ctrl+P', command: 'print' },
{ accelerator: 'Ctrl+Q', command: 'quit' },
{ accelerator: 'Ctrl+Alt+I', command: 'insertTemplate' },
{ accelerator: 'Ctrl+C', command: 'textCopy' },
{ accelerator: 'Ctrl+X', command: 'textCut' },
{ accelerator: 'Ctrl+V', command: 'textPaste' },
{ accelerator: 'Ctrl+A', command: 'textSelectAll' },
{ accelerator: 'Ctrl+B', command: 'textBold' },
{ accelerator: 'Ctrl+I', command: 'textItalic' },
{ accelerator: 'Ctrl+K', command: 'textLink' },
{ accelerator: 'Ctrl+`', command: 'textCode' },
{ accelerator: 'Ctrl+Shift+T', command: 'insertDateTime' },
{ accelerator: 'F6', command: 'focusSearch' },
{ accelerator: 'Ctrl+F', command: 'showLocalSearch' },
{ accelerator: 'Ctrl+Shift+S', command: 'focusElementSideBar' },
{ accelerator: 'Ctrl+Shift+L', command: 'focusElementNoteList' },
{ accelerator: 'Ctrl+Shift+N', command: 'focusElementNoteTitle' },
{ accelerator: 'Ctrl+Shift+B', command: 'focusElementNoteBody' },
{ accelerator: 'F10', command: 'toggleSidebar' },
{ accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' },
{ accelerator: 'Ctrl+0', command: 'zoomActualSize' },
{ accelerator: 'Ctrl+E', command: 'startExternalEditing' },
{ accelerator: 'Ctrl+Alt+T', command: 'setTags' },
{ accelerator: 'Ctrl+,', command: 'config' },
{ accelerator: 'Ctrl+G', command: 'gotoAnything' },
{ accelerator: 'F1', command: 'help' },
],
};
export interface KeymapItem {
accelerator: string;
command: string;
}
interface Keymap {
[command: string]: KeymapItem;
}
export default class KeymapService extends BaseService {
private keymap: Keymap;
private defaultKeymap: KeymapItem[];
constructor() {
super();
// Automatically initialized for the current platform
this.initialize();
}
initialize(platform: string = shim.platformName()) {
this.keysRegExp = keysRegExp;
switch (platform) {
case 'darwin':
this.defaultKeymap = defaultKeymap.darwin;
this.modifiersRegExp = modifiersRegExp.darwin;
break;
default:
this.defaultKeymap = defaultKeymap.default;
this.modifiersRegExp = modifiersRegExp.default;
}
this.keymap = {};
for (let i = 0; i < this.defaultKeymap.length; i++) {
// Make a copy of the KeymapItem before assigning it
// Otherwise we're going to mess up the defaultKeymap array
this.keymap[this.defaultKeymap[i].command] = { ...this.defaultKeymap[i] };
}
}
async loadKeymap(keymapPath: string) {
this.keymapPath = keymapPath; // Used for saving changes later..
if (await fs.exists(keymapPath)) {
this.logger().info(`Loading keymap: ${keymapPath}`);
try {
const keymapFile = await fs.readFile(keymapPath, 'utf-8');
this.setKeymap(JSON.parse(keymapFile));
} catch (err) {
const msg = err.message ? err.message : '';
throw new Error(`Failed to load keymap: ${keymapPath}\n${msg}`);
}
}
}
hasAccelerator(command: string) {
return !!this.keymap[command];
}
getAccelerator(command: string) {
const item = this.keymap[command];
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
else return item.accelerator;
}
setAccelerator(command: string, accelerator: string) {
this.keymap[command].accelerator = accelerator;
}
resetAccelerator(command: string) {
const defaultItem = this.defaultKeymap.find((item => item.command === command));
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
else this.setAccelerator(command, defaultItem.accelerator);
}
setKeymap(customKeymap: KeymapItem[]) {
for (let i = 0; i < customKeymap.length; i++) {
const item = customKeymap[i];
try {
this.validateKeymapItem(item); // Throws if there are any issues in the keymap item
this.setAccelerator(item.command, item.accelerator);
} catch (err) {
throw new Error(`Keymap item ${JSON.stringify(item)} is invalid: ${err.message}`);
}
}
try {
this.validateKeymap(); // Throws whenever there are duplicate Accelerators used in the keymap
} catch (err) {
this.initialize();
throw new Error(`Keymap configuration contains duplicates\n${err.message}`);
}
}
private validateKeymapItem(item: KeymapItem) {
if (!item.hasOwnProperty('command')) {
throw new Error('"command" property is missing');
} else if (!this.keymap.hasOwnProperty(item.command)) {
throw new Error(`"${item.command}" is not a valid command`);
}
if (!item.hasOwnProperty('accelerator')) {
throw new Error('"accelerator" property is missing');
} else if (item.accelerator !== null) {
try {
this.validateAccelerator(item.accelerator);
} catch (err) {
throw new Error(`"${item.accelerator}" is not a valid accelerator`);
}
}
}
private validateKeymap() {
const usedAccelerators = new Set();
for (const item of Object.values(this.keymap)) {
const itemAccelerator = item.accelerator;
if (usedAccelerators.has(itemAccelerator)) {
const originalItem = Object.values(this.keymap).find(_item => _item.accelerator == item.accelerator);
throw new Error(
`Accelerator "${itemAccelerator}" can't be used for both "${item.command}" and "${originalItem.command}" commands\n` +
'You have to change the accelerator for any of above commands'
);
} else if (itemAccelerator !== null) {
usedAccelerators.add(itemAccelerator);
}
}
}
private validateAccelerator(accelerator: string) {
let keyFound = false;
const parts = accelerator.split('+');
const isValid = parts.every((part, index) => {
const isKey = this.keysRegExp.test(part);
const isModifier = this.modifiersRegExp.test(part);
if (isKey) {
// Key must be unique
if (keyFound) return false;
keyFound = true;
}
// Key is required
if (index === (parts.length - 1) && !keyFound) return false;
return isKey || isModifier;
});
if (!isValid) throw new Error(`Accelerator invalid: ${accelerator}`);
}
static instance() {
if (this.instance_) return this.instance_;
this.instance_ = new KeymapService();
return this.instance_;
}
}

View File

@ -91,6 +91,10 @@ class PluginManager {
itemName: item.name,
});
};
if (item.accelerator instanceof Function) {
item.accelerator = item.accelerator();
}
}
output = output.concat(menuItems);