mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-27 10:32:58 +02:00
Desktop: Added KeymapService to manage keyboard shortcuts (#3252)
This commit is contained in:
parent
cc8c200826
commit
88f22fabf7
@ -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
1
.gitignore
vendored
@ -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
|
||||
|
319
CliClient/tests/services_KeymapService.js
Normal file
319
CliClient/tests/services_KeymapService.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
@ -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);
|
||||
|
@ -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'],
|
||||
},
|
||||
],
|
||||
|
@ -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;
|
||||
}
|
||||
|
240
ReactNativeClient/lib/services/KeymapService.ts
Normal file
240
ReactNativeClient/lib/services/KeymapService.ts
Normal 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_;
|
||||
}
|
||||
}
|
@ -91,6 +91,10 @@ class PluginManager {
|
||||
itemName: item.name,
|
||||
});
|
||||
};
|
||||
|
||||
if (item.accelerator instanceof Function) {
|
||||
item.accelerator = item.accelerator();
|
||||
}
|
||||
}
|
||||
|
||||
output = output.concat(menuItems);
|
||||
|
Loading…
Reference in New Issue
Block a user