// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview This class provides a stable interface for initializing,
* querying, and modifying a ChromeVox key map.
*
* An instance contains an object-based bi-directional mapping from key binding
* to a function name of a user command (herein simply called a command).
* A caller is responsible for providing a JSON keymap (a simple Object key
* value structure), which has (key, command) key value pairs.
*
* To retrieve static data about user commands, see CommandStore.
*/
import {TestImportManager} from '/common/testing/test_import_manager.js';
import {Command} from './command.js';
import {CommandStore} from './command_store.js';
import {KeyBinding, KeySequence} from './key_sequence.js';
export class KeyMap {
/** An array of bindings - Commands and KeySequences. */
private bindings_: KeyBinding[];
/**
* Maps a command to a key. This optimizes the process of searching for a
* key sequence when you already know the command.
*/
private commandToKey_: Partial<Record<Command, KeySequence>> = {};
static instance: KeyMap;
private constructor(keyBindings: KeyBinding[]) {
this.bindings_ = keyBindings;
this.buildCommandToKey_();
}
/**
* The number of mappings in the keymap.
* @return The number of mappings.
*/
length(): number {
return this.bindings_.length;
}
/**
* Returns a copy of all KeySequences in this map.
* @return Array of all keys.
*/
keys(): KeySequence[] {
return this.bindings_.map(binding => binding.sequence);
}
/** Returns a shallow copy of the Command, KeySequence bindings. */
bindings(): KeyBinding[] {
return this.bindings_.slice();
}
/** Checks if this key map has a given binding. */
hasBinding(command: Command, sequence: KeySequence): boolean {
if (this.commandToKey_ != null) {
return this.commandToKey_[command] === sequence;
}
return this.bindings_.some(
b => b.command === command && b.sequence.equals(sequence));
}
/** Checks if this key map has a given command. */
hasCommand(command: Command): boolean {
if (this.commandToKey_ != null) {
return this.commandToKey_[command] !== undefined;
}
return this.bindings_.some(b => b.command === command);
}
/** Checks if this key map has a given key. */
hasKey(key: KeySequence): boolean {
return this.bindings_.some(b => b.sequence.equals(key));
}
/** Gets a command given a key. */
commandForKey(key: KeySequence): Command | undefined {
return this.bindings_.find(b => b.sequence.equals(key))?.command;
}
/** Gets a key given a command. */
keyForCommand(command: Command): KeySequence[] {
if (this.commandToKey_ != null) {
// TODO(b/314203187): Not null asserted, check that this is correct.
return [this.commandToKey_[command]!];
}
return this.bindings_.filter(b => b.command === command)
.map(b => b.sequence);
}
/** Convenience method for getting the ChromeVox key map. */
static get(): KeyMap {
if (KeyMap.instance) {
return KeyMap.instance;
}
const keyBindings = CommandStore.getKeyBindings();
KeyMap.instance = new KeyMap(keyBindings);
return KeyMap.instance;
}
/** Builds the map of commands to keys. */
private buildCommandToKey_(): void {
// TODO (dtseng): What about more than one sequence mapped to the same
// command?
for (const binding of this.bindings_) {
if (this.commandToKey_[binding.command] !== undefined) {
// There's at least two key sequences mapped to the same
// command. continue.
continue;
}
this.commandToKey_[binding.command] = binding.sequence;
}
}
}
TestImportManager.exportForTesting(KeyMap);