2020-11-05 18:58:23 +02:00
|
|
|
import eventManager from '../eventManager';
|
|
|
|
import shim from '../shim';
|
|
|
|
import { _ } from '../locale';
|
2020-09-06 14:00:25 +02:00
|
|
|
|
2020-11-05 18:58:23 +02:00
|
|
|
const BaseService = require('./BaseService').default;
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-11-12 20:40:00 +02:00
|
|
|
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|Numlock|Scrolllock|Capslock|num([0-9]|dec|add|sub|mult|div))$/;
|
2020-08-02 13:26:55 +02:00
|
|
|
const modifiersRegExp = {
|
|
|
|
darwin: /^(Ctrl|Option|Shift|Cmd)$/,
|
|
|
|
default: /^(Ctrl|Alt|AltGr|Shift|Super)$/,
|
|
|
|
};
|
|
|
|
|
2020-11-12 20:40:00 +02:00
|
|
|
const keycodeToElectronMap = [
|
|
|
|
'', // [0]
|
|
|
|
'', // [1]
|
|
|
|
'', // [2]
|
|
|
|
'', // [3]
|
|
|
|
'', // [4]
|
|
|
|
'', // [5]
|
|
|
|
'', // [6]
|
|
|
|
'', // [7]
|
|
|
|
'Backspace', // [8]
|
|
|
|
'Tab', // [9]
|
|
|
|
'', // [10]
|
|
|
|
'', // [11]
|
|
|
|
'Clear', // [12]
|
|
|
|
'Enter', // [13]
|
|
|
|
'', // [14]
|
|
|
|
'', // [15]
|
|
|
|
'Shift', // [16]
|
|
|
|
'Ctrl', // [17]
|
|
|
|
'Alt', // [18]
|
|
|
|
'', // [19]
|
|
|
|
'Capslock', // [20]
|
|
|
|
'', // [21]
|
|
|
|
'', // [22]
|
|
|
|
'', // [23]
|
|
|
|
'', // [24]
|
|
|
|
'', // [25]
|
|
|
|
'', // [26]
|
|
|
|
'Esc', // [27]
|
|
|
|
'', // [28]
|
|
|
|
'', // [29]
|
|
|
|
'', // [30]
|
|
|
|
'', // [31]
|
|
|
|
'Space', // [32]
|
|
|
|
'PageUp', // [33]
|
|
|
|
'PageDown', // [34]
|
|
|
|
'End', // [35]
|
|
|
|
'Home', // [36]
|
|
|
|
'Left', // [37]
|
|
|
|
'Up', // [38]
|
|
|
|
'Right', // [39]
|
|
|
|
'Down', // [40]
|
|
|
|
'', // [41]
|
|
|
|
'', // [42]
|
|
|
|
'', // [43]
|
|
|
|
'PrintScreen', // [44]
|
|
|
|
'Insert', // [45]
|
|
|
|
'Delete', // [46]
|
|
|
|
'', // [47]
|
|
|
|
'0', // [48]
|
|
|
|
'1', // [49]
|
|
|
|
'2', // [50]
|
|
|
|
'3', // [51]
|
|
|
|
'4', // [52]
|
|
|
|
'5', // [53]
|
|
|
|
'6', // [54]
|
|
|
|
'7', // [55]
|
|
|
|
'8', // [56]
|
|
|
|
'9', // [57]
|
|
|
|
':', // [58]
|
|
|
|
';', // [59]
|
|
|
|
'<', // [60]
|
|
|
|
'=', // [61]
|
|
|
|
'>', // [62]
|
|
|
|
'?', // [63]
|
|
|
|
'@', // [64]
|
|
|
|
'A', // [65]
|
|
|
|
'B', // [66]
|
|
|
|
'C', // [67]
|
|
|
|
'D', // [68]
|
|
|
|
'E', // [69]
|
|
|
|
'F', // [70]
|
|
|
|
'G', // [71]
|
|
|
|
'H', // [72]
|
|
|
|
'I', // [73]
|
|
|
|
'J', // [74]
|
|
|
|
'K', // [75]
|
|
|
|
'L', // [76]
|
|
|
|
'M', // [77]
|
|
|
|
'N', // [78]
|
|
|
|
'O', // [79]
|
|
|
|
'P', // [80]
|
|
|
|
'Q', // [81]
|
|
|
|
'R', // [82]
|
|
|
|
'S', // [83]
|
|
|
|
'T', // [84]
|
|
|
|
'U', // [85]
|
|
|
|
'V', // [86]
|
|
|
|
'W', // [87]
|
|
|
|
'X', // [88]
|
|
|
|
'Y', // [89]
|
|
|
|
'Z', // [90]
|
|
|
|
'Super', // [91] Super: Windows Key (Windows) or Command Key (Mac)
|
|
|
|
'', // [92]
|
|
|
|
'ContextMenu', // [93]
|
|
|
|
'', // [94]
|
|
|
|
'', // [95]
|
|
|
|
'num0', // [96]
|
|
|
|
'num1', // [97]
|
|
|
|
'num2', // [98]
|
|
|
|
'num3', // [99]
|
|
|
|
'num4', // [100]
|
|
|
|
'num5', // [101]
|
|
|
|
'num6', // [102]
|
|
|
|
'num7', // [103]
|
|
|
|
'num8', // [104]
|
|
|
|
'num9', // [105]
|
|
|
|
'nummult', // [106] *
|
|
|
|
'numadd', // [107] +
|
|
|
|
'', // [108]
|
|
|
|
'numsub', // [109] -
|
|
|
|
'numdec', // [110]
|
|
|
|
'numdiv', // [111] ÷
|
|
|
|
'F1', // [112]
|
|
|
|
'F2', // [113]
|
|
|
|
'F3', // [114]
|
|
|
|
'F4', // [115]
|
|
|
|
'F5', // [116]
|
|
|
|
'F6', // [117]
|
|
|
|
'F7', // [118]
|
|
|
|
'F8', // [119]
|
|
|
|
'F9', // [120]
|
|
|
|
'F10', // [121]
|
|
|
|
'F11', // [122]
|
|
|
|
'F12', // [123]
|
|
|
|
'F13', // [124]
|
|
|
|
'F14', // [125]
|
|
|
|
'F15', // [126]
|
|
|
|
'F16', // [127]
|
|
|
|
'F17', // [128]
|
|
|
|
'F18', // [129]
|
|
|
|
'F19', // [130]
|
|
|
|
'F20', // [131]
|
|
|
|
'F21', // [132]
|
|
|
|
'F22', // [133]
|
|
|
|
'F23', // [134]
|
|
|
|
'F24', // [135]
|
|
|
|
'', // [136]
|
|
|
|
'', // [137]
|
|
|
|
'', // [138]
|
|
|
|
'', // [139]
|
|
|
|
'', // [140]
|
|
|
|
'', // [141]
|
|
|
|
'', // [142]
|
|
|
|
'', // [143]
|
|
|
|
'Numlock', // [144]
|
|
|
|
'Scrolllock', // [145]
|
|
|
|
'', // [146]
|
|
|
|
'', // [147]
|
|
|
|
'', // [148]
|
|
|
|
'', // [149]
|
|
|
|
'', // [150]
|
|
|
|
'', // [151]
|
|
|
|
'', // [152]
|
|
|
|
'', // [153]
|
|
|
|
'', // [154]
|
|
|
|
'', // [155]
|
|
|
|
'', // [156]
|
|
|
|
'', // [157]
|
|
|
|
'', // [158]
|
|
|
|
'', // [159]
|
|
|
|
'', // [160]
|
|
|
|
'!', // [161]
|
|
|
|
'"', // [162]
|
|
|
|
'#', // [163]
|
|
|
|
'$', // [164]
|
|
|
|
'%', // [165]
|
|
|
|
'&', // [166]
|
|
|
|
'_', // [167]
|
|
|
|
'(', // [168]
|
|
|
|
')', // [169]
|
|
|
|
'*', // [170]
|
|
|
|
'Plus', // [171]
|
|
|
|
'|', // [172]
|
|
|
|
'-', // [173]
|
|
|
|
'{', // [174]
|
|
|
|
'}', // [175]
|
|
|
|
'~', // [176]
|
|
|
|
'', // [177]
|
|
|
|
'', // [178]
|
|
|
|
'', // [179]
|
|
|
|
'', // [180]
|
|
|
|
'VolumeMute', // [181]
|
|
|
|
'VolumeDown', // [182]
|
|
|
|
'VolumeUp', // [183]
|
|
|
|
'', // [184]
|
|
|
|
'', // [185]
|
|
|
|
';', // [186]
|
|
|
|
'=', // [187]
|
|
|
|
',', // [188]
|
|
|
|
'-', // [189]
|
|
|
|
'.', // [190]
|
|
|
|
'/', // [191]
|
|
|
|
'`', // [192]
|
|
|
|
'', // [193]
|
|
|
|
'', // [194]
|
|
|
|
'', // [195]
|
|
|
|
'', // [196]
|
|
|
|
'', // [197]
|
|
|
|
'', // [198]
|
|
|
|
'', // [199]
|
|
|
|
'', // [200]
|
|
|
|
'', // [201]
|
|
|
|
'', // [202]
|
|
|
|
'', // [203]
|
|
|
|
'', // [204]
|
|
|
|
'', // [205]
|
|
|
|
'', // [206]
|
|
|
|
'', // [207]
|
|
|
|
'', // [208]
|
|
|
|
'', // [209]
|
|
|
|
'', // [210]
|
|
|
|
'', // [211]
|
|
|
|
'', // [212]
|
|
|
|
'', // [213]
|
|
|
|
'', // [214]
|
|
|
|
'', // [215]
|
|
|
|
'', // [216]
|
|
|
|
'', // [217]
|
|
|
|
'', // [218]
|
|
|
|
'[', // [219]
|
|
|
|
'\\', // [220]
|
|
|
|
']', // [221]
|
|
|
|
'\'', // [222]
|
|
|
|
'', // [223]
|
|
|
|
'', // [224]
|
|
|
|
'AltGr', // [225]
|
|
|
|
'', // [226]
|
|
|
|
'', // [227]
|
|
|
|
'', // [228]
|
|
|
|
'', // [229]
|
|
|
|
'', // [230]
|
|
|
|
'', // [231]
|
|
|
|
'', // [232]
|
|
|
|
'', // [233]
|
|
|
|
'', // [234]
|
|
|
|
'', // [235]
|
|
|
|
'', // [236]
|
|
|
|
'', // [237]
|
|
|
|
'', // [238]
|
|
|
|
'', // [239]
|
|
|
|
'', // [240]
|
|
|
|
'', // [241]
|
|
|
|
'', // [242]
|
|
|
|
'', // [243]
|
|
|
|
'', // [244]
|
|
|
|
'', // [245]
|
|
|
|
'', // [246]
|
|
|
|
'', // [247]
|
|
|
|
'', // [248]
|
|
|
|
'', // [249]
|
|
|
|
'', // [250]
|
|
|
|
'', // [251]
|
|
|
|
'', // [252]
|
|
|
|
'', // [253]
|
|
|
|
'', // [254]
|
|
|
|
'', // [255]
|
|
|
|
];
|
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
const defaultKeymapItems = {
|
2020-08-02 13:26:55 +02:00
|
|
|
darwin: [
|
|
|
|
{ accelerator: 'Cmd+N', command: 'newNote' },
|
|
|
|
{ accelerator: 'Cmd+T', command: 'newTodo' },
|
|
|
|
{ accelerator: 'Cmd+S', command: 'synchronize' },
|
|
|
|
{ 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' },
|
2020-10-18 22:52:10 +02:00
|
|
|
{ accelerator: 'Option+Cmd+S', command: 'toggleSideBar' },
|
2020-09-13 18:21:11 +02:00
|
|
|
{ accelerator: 'Option+Cmd+L', command: 'toggleNoteList' },
|
2020-08-02 13:26:55 +02:00
|
|
|
{ accelerator: 'Cmd+L', command: 'toggleVisiblePanes' },
|
|
|
|
{ accelerator: 'Cmd+0', command: 'zoomActualSize' },
|
2020-10-10 14:32:30 +02:00
|
|
|
{ accelerator: 'Cmd+E', command: 'toggleExternalEditing' },
|
2020-08-02 13:26:55 +02:00
|
|
|
{ accelerator: 'Option+Cmd+T', command: 'setTags' },
|
2020-10-18 22:52:10 +02:00
|
|
|
{ accelerator: 'Cmd+P', command: 'gotoAnything' },
|
|
|
|
{ accelerator: 'Shift+Cmd+P', command: 'commandPalette' },
|
2020-08-02 13:26:55 +02:00
|
|
|
{ accelerator: 'F1', command: 'help' },
|
|
|
|
],
|
|
|
|
default: [
|
|
|
|
{ accelerator: 'Ctrl+N', command: 'newNote' },
|
|
|
|
{ accelerator: 'Ctrl+T', command: 'newTodo' },
|
|
|
|
{ accelerator: 'Ctrl+S', command: 'synchronize' },
|
|
|
|
{ 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' },
|
2020-10-18 22:52:10 +02:00
|
|
|
{ accelerator: 'F10', command: 'toggleSideBar' },
|
2020-09-13 18:21:11 +02:00
|
|
|
{ accelerator: 'F11', command: 'toggleNoteList' },
|
2020-08-02 13:26:55 +02:00
|
|
|
{ accelerator: 'Ctrl+L', command: 'toggleVisiblePanes' },
|
|
|
|
{ accelerator: 'Ctrl+0', command: 'zoomActualSize' },
|
2020-10-10 14:32:30 +02:00
|
|
|
{ accelerator: 'Ctrl+E', command: 'toggleExternalEditing' },
|
2020-08-02 13:26:55 +02:00
|
|
|
{ accelerator: 'Ctrl+Alt+T', command: 'setTags' },
|
|
|
|
{ accelerator: 'Ctrl+,', command: 'config' },
|
2020-10-18 22:52:10 +02:00
|
|
|
{ accelerator: 'Ctrl+P', command: 'gotoAnything' },
|
|
|
|
{ accelerator: 'Ctrl+Shift+P', command: 'commandPalette' },
|
2020-08-02 13:26:55 +02:00
|
|
|
{ accelerator: 'F1', command: 'help' },
|
|
|
|
],
|
|
|
|
};
|
|
|
|
|
|
|
|
export interface KeymapItem {
|
|
|
|
accelerator: string;
|
|
|
|
command: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface Keymap {
|
|
|
|
[command: string]: KeymapItem;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class KeymapService extends BaseService {
|
2020-10-18 22:52:10 +02:00
|
|
|
|
2020-08-02 13:26:55 +02:00
|
|
|
private keymap: Keymap;
|
2020-09-06 14:00:25 +02:00
|
|
|
private platform: string;
|
|
|
|
private customKeymapPath: string;
|
|
|
|
private defaultKeymapItems: KeymapItem[];
|
2020-10-09 19:35:46 +02:00
|
|
|
private lastSaveTime_:number;
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public constructor() {
|
2020-08-02 13:26:55 +02:00
|
|
|
super();
|
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
this.lastSaveTime_ = Date.now();
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public get lastSaveTime():number {
|
2020-10-09 19:35:46 +02:00
|
|
|
return this.lastSaveTime_;
|
|
|
|
}
|
|
|
|
|
2020-10-31 14:25:12 +02:00
|
|
|
// `additionalDefaultCommandNames` will be added to the default keymap
|
|
|
|
// **except** if they are already in it. Basically this is a mechanism
|
|
|
|
// to add all the commands from the command service to the default
|
|
|
|
// keymap.
|
|
|
|
public initialize(additionalDefaultCommandNames:string[] = [], platform: string = shim.platformName()) {
|
2020-09-06 14:00:25 +02:00
|
|
|
this.platform = platform;
|
|
|
|
|
2020-08-02 13:26:55 +02:00
|
|
|
switch (platform) {
|
|
|
|
case 'darwin':
|
2020-10-31 14:25:12 +02:00
|
|
|
this.defaultKeymapItems = defaultKeymapItems.darwin.slice();
|
2020-08-02 13:26:55 +02:00
|
|
|
this.modifiersRegExp = modifiersRegExp.darwin;
|
|
|
|
break;
|
|
|
|
default:
|
2020-10-31 14:25:12 +02:00
|
|
|
this.defaultKeymapItems = defaultKeymapItems.default.slice();
|
2020-08-02 13:26:55 +02:00
|
|
|
this.modifiersRegExp = modifiersRegExp.default;
|
|
|
|
}
|
|
|
|
|
2020-10-31 14:25:12 +02:00
|
|
|
for (const name of additionalDefaultCommandNames) {
|
|
|
|
if (this.defaultKeymapItems.find((item:KeymapItem) => item.command === name)) continue;
|
|
|
|
this.defaultKeymapItems.push({
|
|
|
|
command: name,
|
|
|
|
accelerator: null,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.resetKeymap();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset keymap back to its default values
|
|
|
|
public resetKeymap() {
|
2020-08-02 13:26:55 +02:00
|
|
|
this.keymap = {};
|
2020-09-06 14:00:25 +02:00
|
|
|
for (let i = 0; i < this.defaultKeymapItems.length; i++) {
|
|
|
|
// Keep the original defaultKeymapItems array untouched
|
|
|
|
// Makes it possible to retrieve the original accelerator later, if needed
|
|
|
|
this.keymap[this.defaultKeymapItems[i].command] = { ...this.defaultKeymapItems[i] };
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public async loadCustomKeymap(customKeymapPath: string) {
|
2020-09-06 14:00:25 +02:00
|
|
|
this.customKeymapPath = customKeymapPath; // Useful for saving the changes later
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
if (await shim.fsDriver().exists(customKeymapPath)) {
|
|
|
|
this.logger().info(`KeymapService: Loading keymap from file: ${customKeymapPath}`);
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-10-31 14:25:12 +02:00
|
|
|
const customKeymapFile = (await shim.fsDriver().readFile(customKeymapPath, 'utf-8')).trim();
|
|
|
|
if (!customKeymapFile) return;
|
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
// Custom keymaps are supposed to contain an array of keymap items
|
|
|
|
this.overrideKeymap(JSON.parse(customKeymapFile));
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public async saveCustomKeymap(customKeymapPath: string = this.customKeymapPath) {
|
2020-09-06 14:00:25 +02:00
|
|
|
this.logger().info(`KeymapService: Saving keymap to file: ${customKeymapPath}`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Only the customized keymap items should be saved to the disk
|
|
|
|
const customKeymapItems = this.getCustomKeymapItems();
|
|
|
|
await shim.fsDriver().writeFile(customKeymapPath, JSON.stringify(customKeymapItems, null, 2), 'utf-8');
|
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
this.lastSaveTime_ = Date.now();
|
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
// Refresh the menu items so that the changes are reflected
|
|
|
|
eventManager.emit('keymapChange');
|
|
|
|
} catch (err) {
|
|
|
|
const message = err.message || '';
|
2020-09-15 13:08:25 +02:00
|
|
|
throw new Error(_('Error: %s', message));
|
2020-09-06 14:00:25 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public acceleratorExists(command: string) {
|
2020-08-02 13:26:55 +02:00
|
|
|
return !!this.keymap[command];
|
|
|
|
}
|
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
private convertToPlatform(accelerator:string) {
|
|
|
|
return accelerator
|
|
|
|
.replace(/CmdOrCtrl/g, this.platform === 'darwin' ? 'Cmd' : 'Ctrl')
|
|
|
|
.replace(/Option/g, this.platform === 'darwin' ? 'Option' : 'Alt')
|
|
|
|
.replace(/Alt/g, this.platform === 'darwin' ? 'Option' : 'Alt');
|
|
|
|
}
|
|
|
|
|
2020-10-13 13:57:03 +02:00
|
|
|
public registerCommandAccelerator(commandName:string, accelerator:string) {
|
2020-10-09 19:35:46 +02:00
|
|
|
// If the command is already registered, we don't register it again and
|
|
|
|
// we don't update the accelerator. This is because it might have been
|
|
|
|
// modified by the user and we don't want the plugin to overwrite this.
|
|
|
|
if (this.keymap[commandName]) return;
|
|
|
|
|
2020-10-13 13:57:03 +02:00
|
|
|
if (!commandName) throw new Error('Cannot register an accelerator without a command name');
|
|
|
|
|
2020-10-31 14:25:12 +02:00
|
|
|
const validatedAccelerator = accelerator ? this.convertToPlatform(accelerator) : null;
|
|
|
|
if (validatedAccelerator) this.validateAccelerator(validatedAccelerator);
|
2020-10-09 19:35:46 +02:00
|
|
|
|
|
|
|
this.keymap[commandName] = {
|
|
|
|
command: commandName,
|
|
|
|
accelerator: validatedAccelerator,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public setAccelerator(command: string, accelerator: string) {
|
2020-09-06 14:00:25 +02:00
|
|
|
this.keymap[command].accelerator = accelerator;
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public getAccelerator(command: string) {
|
2020-08-02 13:26:55 +02:00
|
|
|
const item = this.keymap[command];
|
|
|
|
if (!item) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
2020-09-06 14:00:25 +02:00
|
|
|
|
|
|
|
return item.accelerator;
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public getDefaultAccelerator(command: string) {
|
2020-09-06 14:00:25 +02:00
|
|
|
const defaultItem = this.defaultKeymapItems.find((item => item.command === command));
|
|
|
|
if (!defaultItem) throw new Error(`KeymapService: "${command}" command does not exist!`);
|
|
|
|
|
|
|
|
return defaultItem.accelerator;
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public getCommandNames() {
|
2020-09-06 14:00:25 +02:00
|
|
|
return Object.keys(this.keymap);
|
|
|
|
}
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public getKeymapItems() {
|
2020-09-06 14:00:25 +02:00
|
|
|
return Object.values(this.keymap);
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public getCustomKeymapItems() {
|
2020-09-06 14:00:25 +02:00
|
|
|
const customkeymapItems: KeymapItem[] = [];
|
|
|
|
this.defaultKeymapItems.forEach(({ command, accelerator }) => {
|
|
|
|
const currentAccelerator = this.getAccelerator(command);
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
// Only the customized/changed keymap items are neccessary for the custom keymap
|
|
|
|
// Customizations can be merged with the original keymap at the runtime
|
|
|
|
if (this.getAccelerator(command) !== accelerator) {
|
|
|
|
customkeymapItems.push({ command, accelerator: currentAccelerator });
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
2020-09-06 14:00:25 +02:00
|
|
|
});
|
2020-08-02 13:26:55 +02:00
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
for (const commandName in this.keymap) {
|
|
|
|
if (!this.defaultKeymapItems.find((item:KeymapItem) => item.command === commandName)) {
|
|
|
|
customkeymapItems.push(this.keymap[commandName]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
return customkeymapItems;
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public getDefaultKeymapItems() {
|
2020-09-06 14:00:25 +02:00
|
|
|
return [...this.defaultKeymapItems];
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public overrideKeymap(customKeymapItems: KeymapItem[]) {
|
2020-08-02 13:26:55 +02:00
|
|
|
try {
|
2020-09-06 14:00:25 +02:00
|
|
|
for (let i = 0; i < customKeymapItems.length; i++) {
|
|
|
|
const item = customKeymapItems[i];
|
|
|
|
// Validate individual custom keymap items
|
|
|
|
// Throws if there are any issues in the keymap item
|
|
|
|
this.validateKeymapItem(item);
|
2020-10-09 19:35:46 +02:00
|
|
|
|
|
|
|
// If the command does not exist in the keymap, we are loading a new
|
|
|
|
// command accelerator so we need to register it.
|
|
|
|
if (!this.keymap[item.command]) {
|
|
|
|
this.registerCommandAccelerator(item.command, item.accelerator);
|
|
|
|
} else {
|
|
|
|
this.setAccelerator(item.command, item.accelerator);
|
|
|
|
}
|
2020-09-06 14:00:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Validate the entire keymap for duplicates
|
|
|
|
// Throws whenever there are duplicate Accelerators used in the keymap
|
|
|
|
this.validateKeymap();
|
2020-08-02 13:26:55 +02:00
|
|
|
} catch (err) {
|
2020-10-31 14:25:12 +02:00
|
|
|
this.resetKeymap(); // Discard all the changes if there are any issues
|
2020-09-06 14:00:25 +02:00
|
|
|
throw err;
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private validateKeymapItem(item: KeymapItem) {
|
|
|
|
if (!item.hasOwnProperty('command')) {
|
2020-10-09 19:35:46 +02:00
|
|
|
throw new Error(_('"%s" is missing the required "%s" property.', JSON.stringify(item), _('command')));
|
|
|
|
// } else if (!this.keymap.hasOwnProperty(item.command)) {
|
|
|
|
// throw new Error(_('Invalid %s: %s.', _('command'), item.command));
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!item.hasOwnProperty('accelerator')) {
|
2020-10-09 19:35:46 +02:00
|
|
|
throw new Error(_('"%s" is missing the required "%s" property.', JSON.stringify(item), _('accelerator')));
|
2020-08-02 13:26:55 +02:00
|
|
|
} else if (item.accelerator !== null) {
|
|
|
|
try {
|
|
|
|
this.validateAccelerator(item.accelerator);
|
2020-09-06 14:00:25 +02:00
|
|
|
} catch {
|
2020-10-09 19:35:46 +02:00
|
|
|
throw new Error(_('Invalid %s: %s.', _('accelerator'), item.command));
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public validateKeymap(proposedKeymapItem: KeymapItem = null) {
|
2020-08-02 13:26:55 +02:00
|
|
|
const usedAccelerators = new Set();
|
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
// Validate as if the proposed change is already present in the current keymap
|
|
|
|
// Helpful for detecting any errors that'll occur, when the proposed change is performed on the keymap
|
|
|
|
if (proposedKeymapItem) usedAccelerators.add(proposedKeymapItem.accelerator);
|
|
|
|
|
2020-08-02 13:26:55 +02:00
|
|
|
for (const item of Object.values(this.keymap)) {
|
2020-09-06 14:00:25 +02:00
|
|
|
const [itemAccelerator, itemCommand] = [item.accelerator, item.command];
|
|
|
|
if (proposedKeymapItem && itemCommand === proposedKeymapItem.command) continue; // Ignore the original accelerator
|
2020-08-02 13:26:55 +02:00
|
|
|
|
|
|
|
if (usedAccelerators.has(itemAccelerator)) {
|
2020-09-06 14:00:25 +02:00
|
|
|
const originalItem = (proposedKeymapItem && proposedKeymapItem.accelerator === itemAccelerator)
|
|
|
|
? proposedKeymapItem
|
|
|
|
: Object.values(this.keymap).find(_item => _item.accelerator == itemAccelerator);
|
|
|
|
|
|
|
|
throw new Error(_(
|
|
|
|
'Accelerator "%s" is used for "%s" and "%s" commands. This may lead to unexpected behaviour.',
|
|
|
|
itemAccelerator,
|
|
|
|
originalItem.command,
|
|
|
|
itemCommand
|
|
|
|
));
|
|
|
|
} else if (itemAccelerator) {
|
2020-08-02 13:26:55 +02:00
|
|
|
usedAccelerators.add(itemAccelerator);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public validateAccelerator(accelerator: string) {
|
2020-08-02 13:26:55 +02:00
|
|
|
let keyFound = false;
|
|
|
|
|
|
|
|
const parts = accelerator.split('+');
|
|
|
|
const isValid = parts.every((part, index) => {
|
2020-09-06 14:00:25 +02:00
|
|
|
const isKey = keysRegExp.test(part);
|
2020-08-02 13:26:55 +02:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2020-09-06 14:00:25 +02:00
|
|
|
if (!isValid) throw new Error(_('Accelerator "%s" is not valid.', accelerator));
|
|
|
|
}
|
|
|
|
|
2020-11-05 18:58:23 +02:00
|
|
|
public domToElectronAccelerator(event:any) {
|
2020-09-06 14:00:25 +02:00
|
|
|
const parts = [];
|
2020-11-12 20:40:00 +02:00
|
|
|
|
|
|
|
// We use the "keyCode" and not "key" because the modifier keys
|
|
|
|
// would change the "key" value. eg "Option+U" would give "º" as a key instead of "U"
|
|
|
|
const { keyCode, ctrlKey, metaKey, altKey, shiftKey } = event;
|
2020-09-06 14:00:25 +02:00
|
|
|
|
|
|
|
// First, the modifiers
|
2020-11-12 20:40:00 +02:00
|
|
|
// We have to use the following js events, because modifiers won't stick otherwise
|
2020-09-06 14:00:25 +02:00
|
|
|
if (ctrlKey) parts.push('Ctrl');
|
|
|
|
switch (this.platform) {
|
|
|
|
case 'darwin':
|
|
|
|
if (altKey) parts.push('Option');
|
|
|
|
if (shiftKey) parts.push('Shift');
|
|
|
|
if (metaKey) parts.push('Cmd');
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
if (altKey) parts.push('Alt');
|
|
|
|
if (shiftKey) parts.push('Shift');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, the key
|
2020-11-12 20:40:00 +02:00
|
|
|
// String.fromCharCode expects unicode charcodes as an argument; e.keyCode returns javascript keycodes.
|
|
|
|
// Javascript keycodes and unicode charcodes are not the same thing!
|
|
|
|
const electronKey = keycodeToElectronMap[keyCode];
|
|
|
|
if (electronKey && keysRegExp.test(electronKey)) parts.push(electronKey);
|
2020-09-06 14:00:25 +02:00
|
|
|
|
|
|
|
return parts.join('+');
|
|
|
|
}
|
|
|
|
|
|
|
|
public on(eventName: string, callback: Function) {
|
|
|
|
eventManager.on(eventName, callback);
|
|
|
|
}
|
|
|
|
|
|
|
|
public off(eventName: string, callback: Function) {
|
|
|
|
eventManager.off(eventName, callback);
|
2020-08-02 13:26:55 +02:00
|
|
|
}
|
|
|
|
|
2020-10-09 19:35:46 +02:00
|
|
|
private static instance_:KeymapService = null;
|
|
|
|
|
2020-11-10 17:59:30 +02:00
|
|
|
public static destroyInstance() {
|
|
|
|
this.instance_ = null;
|
|
|
|
}
|
|
|
|
|
2020-10-18 22:52:10 +02:00
|
|
|
public static instance():KeymapService {
|
2020-08-02 13:26:55 +02:00
|
|
|
if (this.instance_) return this.instance_;
|
|
|
|
|
|
|
|
this.instance_ = new KeymapService();
|
|
|
|
return this.instance_;
|
|
|
|
}
|
|
|
|
}
|