chromium/chrome/browser/resources/chromeos/accessibility/chromevox/common/key_map.ts

// 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);