diff --git a/browser/src/Services/InputManager/KeyBindingTree.ts b/browser/src/Services/InputManager/KeyBindingTree.ts new file mode 100644 index 0000000000..6d49b96bff --- /dev/null +++ b/browser/src/Services/InputManager/KeyBindingTree.ts @@ -0,0 +1,42 @@ +export type ActionFunction = () => boolean + +export type ActionOrCommand = string | ActionFunction + +export type FilterFunction = () => boolean + +export type KeyBinding = { + action: ActionOrCommand + filter?: FilterFunction +} + +export type KeyBindingTree = { + children: { [keyBinding: string]: KeyBindingTree } + bindings: KeyBinding[] +} + +export const create = (): KeyBindingTree => { + return { + children: {}, + bindings: [], + } +} + +export const isPotentialChord = (keys: string[], tree: KeyBindingTree): boolean => { + return false +} + +export const getKeyBindingsForChord = (keys: string): KeyBinding[] => { + return [] +} + +export const setKeyBindingsForChord = ( + keys: string, + binding: KeyBinding[], + tree: KeyBindingTree, +): KeyBindingTree => { + return tree +} + +export const isTerminal = (keys: string[], tree: KeyBindingTree): boolean => { + return false +} diff --git a/browser/src/Services/InputManager.ts b/browser/src/Services/InputManager/index.ts similarity index 75% rename from browser/src/Services/InputManager.ts rename to browser/src/Services/InputManager/index.ts index 89daa64858..bdeffcb515 100644 --- a/browser/src/Services/InputManager.ts +++ b/browser/src/Services/InputManager/index.ts @@ -1,6 +1,7 @@ import * as Oni from "oni-api" +import { Event, IEvent } from "oni-types" -import { commandManager } from "./CommandManager" +import { commandManager } from "./../CommandManager" export type ActionFunction = () => boolean @@ -8,27 +9,18 @@ export type ActionOrCommand = string | ActionFunction export type FilterFunction = () => boolean -import { IKeyChord, parseKeysFromVimString } from "./../Input/KeyParser" - -export interface KeyBinding { - action: ActionOrCommand - filter?: FilterFunction -} - -export interface KeyBindingMap { - [key: string]: KeyBinding[] -} +import * as KeyBindingTree from "./KeyBindingTree" const MAX_DELAY_BETWEEN_KEY_CHORD = 250 /* milliseconds */ const MAX_CHORD_SIZE = 4 -import { KeyboardResolver } from "./../Input/Keyboard/KeyboardResolver" +import { KeyboardResolver } from "./../../Input/Keyboard/KeyboardResolver" import { getMetaKeyResolver, ignoreMetaKeyResolver, remapResolver, -} from "./../Input/Keyboard/Resolvers" +} from "./../../Input/Keyboard/Resolvers" export interface KeyPressInfo { keyChord: string @@ -64,6 +56,27 @@ export class InputManager implements Oni.Input.InputManager { private _boundKeys: KeyBindingMap = {} private _resolver: KeyboardResolver private _keys: KeyPressInfo[] = [] + private _onUnhandledKeyEvent = new Event() + + private _bindingTree: KeyBindingTree.KeyBindingTree = KeyBindingTree.create() + + /** + * Event that is dispatched when a potential chorded input was + * picked up, but the chord ended up not being completed + * + * If there were multiple keys in the chord that were not handled, + * this event will be dispatched multiple times, in order. + * + * An example would be: + * `input.bind("abc")` + * + * And then the user typing: `abd`. Initially, the 'a' and 'b' + * characters would be swallowed, but upon the 'd' keypress, + * the 'a' and 'b' key events would be dispatched. + */ + public get onUnhandledKey(): IEvent { + return this._onUnhandledKeyEvent + } constructor() { this._resolver = new KeyboardResolver() @@ -87,10 +100,19 @@ export class InputManager implements Oni.Input.InputManager { } const normalizedKeyChord = keyChord.toLowerCase() - const currentBinding = this._boundKeys[normalizedKeyChord] || [] + + // const currentBinding = this._boundKeys[normalizedKeyChord] || [] const newBinding = { action, filter: filterFunction } - this._boundKeys[normalizedKeyChord] = [...currentBinding, newBinding] + const currentBindings = getKeyBindingsForChord(keyChord) + + this._bindingTree = setKeyBindingsForChord( + keyChord, + [...currentBindings, newBinding], + this._bindingTree, + ) + + // this._boundKeys[normalizedKeyChord] = [...currentBinding, newBinding] return () => { const existingBindings = this._boundKeys[normalizedKeyChord] @@ -107,12 +129,15 @@ export class InputManager implements Oni.Input.InputManager { return } - const normalizedKeyChord = keyChord.toLowerCase() - this._boundKeys[normalizedKeyChord] = [] + this._bindingTree = setKeyBindingsForChord(keyChord, [], this._bindingTree) + + // const normalizedKeyChord = keyChord.toLowerCase() + // this._boundKeys[normalizedKeyChord] = [] } public unbindAll() { - this._boundKeys = {} + this._bindingTree = { children: [] } + // this._boundKeys = {} } /** diff --git a/browser/test/Input/InputManagerTests.ts b/browser/test/Input/InputManagerTests.ts index d478655957..d5391018e2 100644 --- a/browser/test/Input/InputManagerTests.ts +++ b/browser/test/Input/InputManagerTests.ts @@ -130,6 +130,62 @@ describe("InputManager", () => { }) }) + describe("onUnhandledKey", () => { + let inputManager: InputManager + let unhandledKeys: string[] + + beforeEach(() => { + inputManager = new InputManager() + unhandledKeys = [] + inputManager.onUnhandledKey.subscribe(key => unhandledKeys.push(key)) + }) + + it("doesn't dispatch if key wasn't bound", () => { + const val = inputManager.handleKey("a", 1) + assert.strictEqual(val, false) + assert.deepEqual(unhandledKeys, []) + }) + + it("doesn't dispatch if chord was successfully executed", () => { + let hitCount = 0 + inputManager.bind("abc", () => { + hitCount++ + return true + }) + + let h1 = inputManager.handleKey("a") + let h2 = inputManager.handleKey("b") + let h3 = inputManager.handleKey("c") + + assert.strictEqual(h1, true) + assert.strictEqual(h2, true) + assert.strictEqual(h3, true) + + assert.strictEqual(hitCount, 1) + assert.deepEqual(unhandledKeys, []) + }) + + it("dispatches key if chord was missed", () => { + let hitCount = 0 + inputManager.bind("abc", () => { + hitCount++ + return true + }) + + let h1 = inputManager.handleKey("a") + let h2 = inputManager.handleKey("b") + let h3 = inputManager.handleKey("d") + + assert.strictEqual(h1, true) + assert.strictEqual(h2, true) + assert.strictEqual(h3, false) + + assert.strictEqual(hitCount, 1) + + assert.deepEqual(["a", "b"], unhandledKeys) + }) + }) + describe("getRecentKeyPresses", () => { const createKeyPressInfo = (keyChord: string, time: number): KeyPressInfo => ({ keyChord, diff --git a/browser/test/Services/InputManager/KeyBindingTreeTests.ts b/browser/test/Services/InputManager/KeyBindingTreeTests.ts new file mode 100644 index 0000000000..6e3ab57117 --- /dev/null +++ b/browser/test/Services/InputManager/KeyBindingTreeTests.ts @@ -0,0 +1,48 @@ +/** + * KeyBindingTreeTests.ts + */ + +import * as assert from "assert" + +import * as KeyBindingTree from "./../../../src/Services/InputManager/KeyBindingTree" + +describe("KeyBindingTreeTests", () => { + describe("create", () => { + it("has no bindings or children", () => { + const kbt = KeyBindingTree.create() + + assert.deepEqual(kbt.children, {}) + assert.deepEqual(kbt.bindings, []) + }) + }) + + describe("isPotentialChord", () => { + it("returns false for unbound key", () => { + assert.ok(false) + }) + + it("returns true if part of chord, but false if complete", () => { + assert.ok(false) + }) + }) + + describe("getKeyBindingsForChord", () => { + it("returns empty array if no bindings", () => { + assert.ok(false) + }) + + it("gives value for single item chord", () => { + assert.ok(false) + }) + }) + + describe("isTerminal", () => { + it("returns true for single item with no chorded bindings", () => { + assert.ok(false) + }) + + it("returns false for key that has potential bindings", () => { + assert.ok(false) + }) + }) +})