diff --git a/src/KeyBindingsDefaults.ts b/src/KeyBindingsDefaults.ts new file mode 100644 index 0000000000..ed98a06c7f --- /dev/null +++ b/src/KeyBindingsDefaults.ts @@ -0,0 +1,384 @@ +import { AutocompleteAction, IKeyBindingsProvider, KeyBinding, MessageComposerAction, NavigationAction, RoomAction, + RoomListAction } from "./KeyBindingsManager"; +import { isMac, Key } from "./Keyboard"; +import SettingsStore from "./settings/SettingsStore"; + +const messageComposerBindings = (): KeyBinding[] => { + const bindings: KeyBinding[] = [ + { + action: MessageComposerAction.SelectPrevSendHistory, + keyCombo: { + key: Key.ARROW_UP, + altKey: true, + ctrlKey: true, + }, + }, + { + action: MessageComposerAction.SelectNextSendHistory, + keyCombo: { + key: Key.ARROW_DOWN, + altKey: true, + ctrlKey: true, + }, + }, + { + action: MessageComposerAction.EditPrevMessage, + keyCombo: { + key: Key.ARROW_UP, + }, + }, + { + action: MessageComposerAction.EditNextMessage, + keyCombo: { + key: Key.ARROW_DOWN, + }, + }, + { + action: MessageComposerAction.CancelEditing, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: MessageComposerAction.FormatBold, + keyCombo: { + key: Key.B, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.FormatItalics, + keyCombo: { + key: Key.I, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.FormatQuote, + keyCombo: { + key: Key.GREATER_THAN, + ctrlOrCmd: true, + shiftKey: true, + }, + }, + { + action: MessageComposerAction.EditUndo, + keyCombo: { + key: Key.Z, + ctrlOrCmd: true, + }, + }, + // Note: the following two bindings also work with just HOME and END, add them here? + { + action: MessageComposerAction.MoveCursorToStart, + keyCombo: { + key: Key.HOME, + ctrlOrCmd: true, + }, + }, + { + action: MessageComposerAction.MoveCursorToEnd, + keyCombo: { + key: Key.END, + ctrlOrCmd: true, + }, + }, + ]; + if (isMac) { + bindings.push({ + action: MessageComposerAction.EditRedo, + keyCombo: { + key: Key.Z, + ctrlOrCmd: true, + shiftKey: true, + }, + }); + } else { + bindings.push({ + action: MessageComposerAction.EditRedo, + keyCombo: { + key: Key.Y, + ctrlOrCmd: true, + }, + }); + } + if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) { + bindings.push({ + action: MessageComposerAction.Send, + keyCombo: { + key: Key.ENTER, + ctrlOrCmd: true, + }, + }); + bindings.push({ + action: MessageComposerAction.NewLine, + keyCombo: { + key: Key.ENTER, + }, + }); + } else { + bindings.push({ + action: MessageComposerAction.Send, + keyCombo: { + key: Key.ENTER, + }, + }); + bindings.push({ + action: MessageComposerAction.NewLine, + keyCombo: { + key: Key.ENTER, + shiftKey: true, + }, + }); + if (isMac) { + bindings.push({ + action: MessageComposerAction.NewLine, + keyCombo: { + key: Key.ENTER, + altKey: true, + }, + }); + } + } + return bindings; +} + +const autocompleteBindings = (): KeyBinding[] => { + return [ + { + action: AutocompleteAction.ApplySelection, + keyCombo: { + key: Key.TAB, + }, + }, + { + action: AutocompleteAction.ApplySelection, + keyCombo: { + key: Key.TAB, + ctrlKey: true, + }, + }, + { + action: AutocompleteAction.ApplySelection, + keyCombo: { + key: Key.TAB, + shiftKey: true, + }, + }, + { + action: AutocompleteAction.Cancel, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: AutocompleteAction.PrevSelection, + keyCombo: { + key: Key.ARROW_UP, + }, + }, + { + action: AutocompleteAction.NextSelection, + keyCombo: { + key: Key.ARROW_DOWN, + }, + }, + ]; +} + +const roomListBindings = (): KeyBinding[] => { + return [ + { + action: RoomListAction.ClearSearch, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: RoomListAction.PrevRoom, + keyCombo: { + key: Key.ARROW_UP, + }, + }, + { + action: RoomListAction.NextRoom, + keyCombo: { + key: Key.ARROW_DOWN, + }, + }, + { + action: RoomListAction.SelectRoom, + keyCombo: { + key: Key.ENTER, + }, + }, + { + action: RoomListAction.CollapseSection, + keyCombo: { + key: Key.ARROW_LEFT, + }, + }, + { + action: RoomListAction.ExpandSection, + keyCombo: { + key: Key.ARROW_RIGHT, + }, + }, + ]; +} + +const roomBindings = (): KeyBinding[] => { + const bindings = [ + { + action: RoomAction.FocusRoomSearch, + keyCombo: { + key: Key.K, + ctrlOrCmd: true, + }, + }, + { + action: RoomAction.ScrollUp, + keyCombo: { + key: Key.PAGE_UP, + }, + }, + { + action: RoomAction.RoomScrollDown, + keyCombo: { + key: Key.PAGE_DOWN, + }, + }, + { + action: RoomAction.DismissReadMarker, + keyCombo: { + key: Key.ESCAPE, + }, + }, + { + action: RoomAction.JumpToOldestUnread, + keyCombo: { + key: Key.PAGE_UP, + shiftKey: true, + }, + }, + { + action: RoomAction.UploadFile, + keyCombo: { + key: Key.U, + ctrlOrCmd: true, + shiftKey: true, + }, + }, + { + action: RoomAction.JumpToFirstMessage, + keyCombo: { + key: Key.HOME, + ctrlKey: true, + }, + }, + { + action: RoomAction.JumpToLatestMessage, + keyCombo: { + key: Key.END, + ctrlKey: true, + }, + }, + ]; + + if (SettingsStore.getValue('ctrlFForSearch')) { + bindings.push({ + action: RoomAction.FocusSearch, + keyCombo: { + key: Key.F, + ctrlOrCmd: true, + }, + }); + } + + return bindings; +} + +const navigationBindings = (): KeyBinding[] => { + return [ + { + action: NavigationAction.ToggleRoomSidePanel, + keyCombo: { + key: Key.PERIOD, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleUserMenu, + // Ideally this would be CTRL+P for "Profile", but that's + // taken by the print dialog. CTRL+I for "Information" + // was previously chosen but conflicted with italics in + // composer, so CTRL+` it is + keyCombo: { + key: Key.BACKTICK, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleShortCutDialog, + keyCombo: { + key: Key.SLASH, + ctrlOrCmd: true, + }, + }, + { + action: NavigationAction.ToggleShortCutDialog, + keyCombo: { + key: Key.SLASH, + ctrlOrCmd: true, + shiftKey: true, + }, + }, + { + action: NavigationAction.GoToHome, + keyCombo: { + key: Key.H, + ctrlOrCmd: true, + altKey: true, + }, + }, + + { + action: NavigationAction.SelectPrevRoom, + keyCombo: { + key: Key.ARROW_UP, + altKey: true, + }, + }, + { + action: NavigationAction.SelectNextRoom, + keyCombo: { + key: Key.ARROW_DOWN, + altKey: true, + }, + }, + { + action: NavigationAction.SelectPrevUnreadRoom, + keyCombo: { + key: Key.ARROW_UP, + altKey: true, + shiftKey: true, + }, + }, + { + action: NavigationAction.SelectNextUnreadRoom, + keyCombo: { + key: Key.ARROW_DOWN, + altKey: true, + shiftKey: true, + }, + }, + ] +} + +export const defaultBindingProvider: IKeyBindingsProvider = { + getMessageComposerBindings: messageComposerBindings, + getAutocompleteBindings: autocompleteBindings, + getRoomListBindings: roomListBindings, + getRoomBindings: roomBindings, + getNavigationBindings: navigationBindings, +} diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts index 00e16ce2ab..cf11fc711f 100644 --- a/src/KeyBindingsManager.ts +++ b/src/KeyBindingsManager.ts @@ -1,5 +1,5 @@ -import { isMac, Key } from './Keyboard'; -import SettingsStore from './settings/SettingsStore'; +import { defaultBindingProvider } from './KeyBindingsDefaults'; +import { isMac } from './Keyboard'; /** Actions for the chat message composer component */ export enum MessageComposerAction { @@ -124,371 +124,6 @@ export type KeyBinding = { keyCombo: KeyCombo; } -const messageComposerBindings = (): KeyBinding[] => { - const bindings: KeyBinding[] = [ - { - action: MessageComposerAction.SelectPrevSendHistory, - keyCombo: { - key: Key.ARROW_UP, - altKey: true, - ctrlKey: true, - }, - }, - { - action: MessageComposerAction.SelectNextSendHistory, - keyCombo: { - key: Key.ARROW_DOWN, - altKey: true, - ctrlKey: true, - }, - }, - { - action: MessageComposerAction.EditPrevMessage, - keyCombo: { - key: Key.ARROW_UP, - }, - }, - { - action: MessageComposerAction.EditNextMessage, - keyCombo: { - key: Key.ARROW_DOWN, - }, - }, - { - action: MessageComposerAction.CancelEditing, - keyCombo: { - key: Key.ESCAPE, - }, - }, - { - action: MessageComposerAction.FormatBold, - keyCombo: { - key: Key.B, - ctrlOrCmd: true, - }, - }, - { - action: MessageComposerAction.FormatItalics, - keyCombo: { - key: Key.I, - ctrlOrCmd: true, - }, - }, - { - action: MessageComposerAction.FormatQuote, - keyCombo: { - key: Key.GREATER_THAN, - ctrlOrCmd: true, - shiftKey: true, - }, - }, - { - action: MessageComposerAction.EditUndo, - keyCombo: { - key: Key.Z, - ctrlOrCmd: true, - }, - }, - // Note: the following two bindings also work with just HOME and END, add them here? - { - action: MessageComposerAction.MoveCursorToStart, - keyCombo: { - key: Key.HOME, - ctrlOrCmd: true, - }, - }, - { - action: MessageComposerAction.MoveCursorToEnd, - keyCombo: { - key: Key.END, - ctrlOrCmd: true, - }, - }, - ]; - if (isMac) { - bindings.push({ - action: MessageComposerAction.EditRedo, - keyCombo: { - key: Key.Z, - ctrlOrCmd: true, - shiftKey: true, - }, - }); - } else { - bindings.push({ - action: MessageComposerAction.EditRedo, - keyCombo: { - key: Key.Y, - ctrlOrCmd: true, - }, - }); - } - if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) { - bindings.push({ - action: MessageComposerAction.Send, - keyCombo: { - key: Key.ENTER, - ctrlOrCmd: true, - }, - }); - bindings.push({ - action: MessageComposerAction.NewLine, - keyCombo: { - key: Key.ENTER, - }, - }); - } else { - bindings.push({ - action: MessageComposerAction.Send, - keyCombo: { - key: Key.ENTER, - }, - }); - bindings.push({ - action: MessageComposerAction.NewLine, - keyCombo: { - key: Key.ENTER, - shiftKey: true, - }, - }); - if (isMac) { - bindings.push({ - action: MessageComposerAction.NewLine, - keyCombo: { - key: Key.ENTER, - altKey: true, - }, - }); - } - } - return bindings; -} - -const autocompleteBindings = (): KeyBinding[] => { - return [ - { - action: AutocompleteAction.ApplySelection, - keyCombo: { - key: Key.TAB, - }, - }, - { - action: AutocompleteAction.ApplySelection, - keyCombo: { - key: Key.TAB, - ctrlKey: true, - }, - }, - { - action: AutocompleteAction.ApplySelection, - keyCombo: { - key: Key.TAB, - shiftKey: true, - }, - }, - { - action: AutocompleteAction.Cancel, - keyCombo: { - key: Key.ESCAPE, - }, - }, - { - action: AutocompleteAction.PrevSelection, - keyCombo: { - key: Key.ARROW_UP, - }, - }, - { - action: AutocompleteAction.NextSelection, - keyCombo: { - key: Key.ARROW_DOWN, - }, - }, - ]; -} - -const roomListBindings = (): KeyBinding[] => { - return [ - { - action: RoomListAction.ClearSearch, - keyCombo: { - key: Key.ESCAPE, - }, - }, - { - action: RoomListAction.PrevRoom, - keyCombo: { - key: Key.ARROW_UP, - }, - }, - { - action: RoomListAction.NextRoom, - keyCombo: { - key: Key.ARROW_DOWN, - }, - }, - { - action: RoomListAction.SelectRoom, - keyCombo: { - key: Key.ENTER, - }, - }, - { - action: RoomListAction.CollapseSection, - keyCombo: { - key: Key.ARROW_LEFT, - }, - }, - { - action: RoomListAction.ExpandSection, - keyCombo: { - key: Key.ARROW_RIGHT, - }, - }, - ]; -} - -const roomBindings = (): KeyBinding[] => { - const bindings = [ - { - action: RoomAction.FocusRoomSearch, - keyCombo: { - key: Key.K, - ctrlOrCmd: true, - }, - }, - { - action: RoomAction.ScrollUp, - keyCombo: { - key: Key.PAGE_UP, - }, - }, - { - action: RoomAction.RoomScrollDown, - keyCombo: { - key: Key.PAGE_DOWN, - }, - }, - { - action: RoomAction.DismissReadMarker, - keyCombo: { - key: Key.ESCAPE, - }, - }, - { - action: RoomAction.UploadFile, - keyCombo: { - key: Key.U, - ctrlOrCmd: true, - shiftKey: true, - }, - }, - { - action: RoomAction.JumpToFirstMessage, - keyCombo: { - key: Key.HOME, - ctrlKey: true, - }, - }, - { - action: RoomAction.JumpToLatestMessage, - keyCombo: { - key: Key.END, - ctrlKey: true, - }, - }, - ]; - - if (SettingsStore.getValue('ctrlFForSearch')) { - bindings.push({ - action: RoomAction.FocusSearch, - keyCombo: { - key: Key.F, - ctrlOrCmd: true, - }, - }); - } - - return bindings; -} - -const navigationBindings = (): KeyBinding[] => { - return [ - { - action: NavigationAction.ToggleRoomSidePanel, - keyCombo: { - key: Key.PERIOD, - ctrlOrCmd: true, - }, - }, - { - action: NavigationAction.ToggleUserMenu, - // Ideally this would be CTRL+P for "Profile", but that's - // taken by the print dialog. CTRL+I for "Information" - // was previously chosen but conflicted with italics in - // composer, so CTRL+` it is - keyCombo: { - key: Key.BACKTICK, - ctrlOrCmd: true, - }, - }, - { - action: NavigationAction.ToggleShortCutDialog, - keyCombo: { - key: Key.SLASH, - ctrlOrCmd: true, - }, - }, - { - action: NavigationAction.ToggleShortCutDialog, - keyCombo: { - key: Key.SLASH, - ctrlOrCmd: true, - shiftKey: true, - }, - }, - { - action: NavigationAction.GoToHome, - keyCombo: { - key: Key.H, - ctrlOrCmd: true, - altKey: true, - }, - }, - - { - action: NavigationAction.SelectPrevRoom, - keyCombo: { - key: Key.ARROW_UP, - altKey: true, - }, - }, - { - action: NavigationAction.SelectNextRoom, - keyCombo: { - key: Key.ARROW_DOWN, - altKey: true, - }, - }, - { - action: NavigationAction.SelectPrevUnreadRoom, - keyCombo: { - key: Key.ARROW_UP, - altKey: true, - shiftKey: true, - }, - }, - { - action: NavigationAction.SelectNextUnreadRoom, - keyCombo: { - key: Key.ARROW_DOWN, - altKey: true, - shiftKey: true, - }, - }, - ] -} - /** * Helper method to check if a KeyboardEvent matches a KeyCombo * @@ -541,42 +176,63 @@ export function isKeyComboMatch(ev: KeyboardEvent | React.KeyboardEvent, combo: return true; } + +export type KeyBindingGetter = () => KeyBinding[]; + +export interface IKeyBindingsProvider { + getMessageComposerBindings: KeyBindingGetter; + getAutocompleteBindings: KeyBindingGetter; + getRoomListBindings: KeyBindingGetter; + getRoomBindings: KeyBindingGetter; + getNavigationBindings: KeyBindingGetter; +} + export class KeyBindingsManager { + /** + * List of key bindings providers. + * + * Key bindings from the first provider(s) in the list will have precedence over key bindings from later providers. + * + * To overwrite the default key bindings add a new providers before the default provider, e.g. a provider for + * customized key bindings. + */ + bindingsProviders: IKeyBindingsProvider[] = [ + defaultBindingProvider, + ]; + /** * Finds a matching KeyAction for a given KeyboardEvent */ - private getAction(bindings: KeyBinding[], ev: KeyboardEvent | React.KeyboardEvent) + private getAction(getters: KeyBindingGetter[], ev: KeyboardEvent | React.KeyboardEvent) : T | undefined { - const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); - if (binding) { - return binding.action; + for (const getter of getters) { + const bindings = getter(); + const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac)); + if (binding) { + return binding.action; + } } return undefined; } getMessageComposerAction(ev: KeyboardEvent | React.KeyboardEvent): MessageComposerAction | undefined { - const bindings = messageComposerBindings(); - return this.getAction(bindings, ev); + return this.getAction(this.bindingsProviders.map(it => it.getMessageComposerBindings), ev); } getAutocompleteAction(ev: KeyboardEvent | React.KeyboardEvent): AutocompleteAction | undefined { - const bindings = autocompleteBindings(); - return this.getAction(bindings, ev); + return this.getAction(this.bindingsProviders.map(it => it.getAutocompleteBindings), ev); } getRoomListAction(ev: KeyboardEvent | React.KeyboardEvent): RoomListAction | undefined { - const bindings = roomListBindings(); - return this.getAction(bindings, ev); + return this.getAction(this.bindingsProviders.map(it => it.getRoomListBindings), ev); } getRoomAction(ev: KeyboardEvent | React.KeyboardEvent): RoomAction | undefined { - const bindings = roomBindings(); - return this.getAction(bindings, ev); + return this.getAction(this.bindingsProviders.map(it => it.getRoomBindings), ev); } getNavigationAction(ev: KeyboardEvent | React.KeyboardEvent): NavigationAction | undefined { - const bindings = navigationBindings(); - return this.getAction(bindings, ev); + return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev); } }