chromium/ash/public/cpp/accelerators_util.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/public/cpp/accelerators_util.h"

#include <iterator>
#include <string>

#include "ash/public/cpp/accelerator_keycode_lookup_cache.h"
#include "base/containers/fixed_flat_set.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "ui/base/accelerators/ash/right_alt_event_property.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/dom_codes_array.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/keycodes/dom_us_layout_data.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/ozone/layout/keyboard_layout_engine.h"
#include "ui/events/ozone/layout/keyboard_layout_engine_manager.h"

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
#include "chromeos/ash/resources/internal/strings/grit/ash_internal_strings.h"
#endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)

namespace ash {

namespace {

using KeyCodeLookupEntry = AcceleratorKeycodeLookupCache::KeyCodeLookupEntry;

constexpr char kUnidentifiedKeyString[] = "Unidentified";

// Dead keys work by combining two consecutive keystrokes together. The first
// keystroke does not produce an output character, it acts as a one-shot
// modifier for a subsequent keystroke. So for example on a German keyboard,
// pressing the acute ´ dead key, then pressing the letter e will produce é.
// The first character is called the combining character and does not produce
// an output glyph. This table maps the combining character to a string
// containing the non-combining equivalent that can be displayed.
std::u16string GetStringForDeadKey(ui::DomKey dom_key) {
  DCHECK(dom_key.IsDeadKey());
  int32_t ch = dom_key.ToDeadKeyCombiningCharacter();
  switch (ch) {
    // Combining grave.
    case 0x300:
      return u"`";
    // Combining acute.
    case 0x301:
      return u"´";
    // Combining circumflex.
    case 0x302:
      return u"^";
    // Combining tilde.
    case 0x303:
      return u"~";
    // Combining diaeresis.
    case 0x308:
      return u"¨";
    default:
      break;
  }

  LOG(WARNING) << "No mapping for dead key: " << ch;
  return base::UTF8ToUTF16(ui::KeycodeConverter::DomKeyToKeyString(dom_key));
}

// This map is for KeyboardCodes that don't return a key_display from
// `KeycodeToKeyString`. The string values here were arbitrarily chosen
// based on the VKEY enum name.
const base::flat_map<ui::KeyboardCode, std::u16string>& GetKeyDisplayMap() {
  static auto key_display_map =
      base::NoDestructor(base::flat_map<ui::KeyboardCode, std::u16string>({
          {ui::KeyboardCode::VKEY_MICROPHONE_MUTE_TOGGLE,
           u"MicrophoneMuteToggle"},
          {ui::KeyboardCode::VKEY_KBD_BACKLIGHT_TOGGLE,
           u"KeyboardBacklightToggle"},
          {ui::KeyboardCode::VKEY_KBD_BRIGHTNESS_UP, u"KeyboardBrightnessUp"},
          {ui::KeyboardCode::VKEY_KBD_BRIGHTNESS_DOWN,
           u"KeyboardBrightnessDown"},
          {ui::KeyboardCode::VKEY_SLEEP, u"Sleep"},
          {ui::KeyboardCode::VKEY_NEW, u"NewTab"},
          {ui::KeyboardCode::VKEY_PRIVACY_SCREEN_TOGGLE,
           u"PrivacyScreenToggle"},
          {ui::KeyboardCode::VKEY_ALL_APPLICATIONS, u"ViewAllApps"},
          {ui::KeyboardCode::VKEY_DICTATE, u"EnableOrToggleDictation"},
          {ui::KeyboardCode::VKEY_WLAN, u"ToggleWifi"},
          {ui::KeyboardCode::VKEY_EMOJI_PICKER, u"EmojiPicker"},
          {ui::KeyboardCode::VKEY_MENU, u"alt"},
          {ui::KeyboardCode::VKEY_HOME, u"home"},
          {ui::KeyboardCode::VKEY_END, u"end"},
          {ui::KeyboardCode::VKEY_DELETE, u"delete"},
          {ui::KeyboardCode::VKEY_INSERT, u"insert"},
          {ui::KeyboardCode::VKEY_PRIOR, u"page up"},
          {ui::KeyboardCode::VKEY_NEXT, u"page down"},
          {ui::KeyboardCode::VKEY_SPACE, u"space"},
          {ui::KeyboardCode::VKEY_TAB, u"tab"},
          {ui::KeyboardCode::VKEY_ESCAPE, u"esc"},
          {ui::KeyboardCode::VKEY_RETURN, u"enter"},
          {ui::KeyboardCode::VKEY_BACK, u"backspace"},
          {ui::KeyboardCode::VKEY_MEDIA_PLAY, u"MediaPlay"},
          {ui::KeyboardCode::VKEY_NUMPAD0, u"numpad 0"},
          {ui::KeyboardCode::VKEY_NUMPAD1, u"numpad 1"},
          {ui::KeyboardCode::VKEY_NUMPAD2, u"numpad 2"},
          {ui::KeyboardCode::VKEY_NUMPAD3, u"numpad 3"},
          {ui::KeyboardCode::VKEY_NUMPAD4, u"numpad 4"},
          {ui::KeyboardCode::VKEY_NUMPAD5, u"numpad 5"},
          {ui::KeyboardCode::VKEY_NUMPAD6, u"numpad 6"},
          {ui::KeyboardCode::VKEY_NUMPAD7, u"numpad 7"},
          {ui::KeyboardCode::VKEY_NUMPAD8, u"numpad 8"},
          {ui::KeyboardCode::VKEY_NUMPAD9, u"numpad 9"},
          {ui::KeyboardCode::VKEY_ADD, u"numpad +"},
          {ui::KeyboardCode::VKEY_DECIMAL, u"numpad ."},
          {ui::KeyboardCode::VKEY_DIVIDE, u"numpad /"},
          {ui::KeyboardCode::VKEY_MULTIPLY, u"numpad *"},
          {ui::KeyboardCode::VKEY_SUBTRACT, u"numpad -"},
          {ui::KeyboardCode::VKEY_CAPITAL, u"caps lock"},
          {ui::KeyboardCode::VKEY_ACCESSIBILITY, u"Accessibility"},
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
          {ui::KeyboardCode::VKEY_RIGHT_ALT, u"RightAlt"},
#else
          {ui::KeyboardCode::VKEY_RIGHT_ALT, u"right alt"},
#endif
      }));
  return *key_display_map;
}

bool IsValidDomCode(ui::DomCode dom_code) {
  return ui::KeycodeConverter::InvalidNativeKeycode() !=
         ui::KeycodeConverter::UsbKeycodeToNativeKeycode(
             static_cast<int32_t>(dom_code));
}

bool IsAlphaOrPunctuationKey(ui::KeyboardCode key_code) {
  if (key_code >= ui::VKEY_A && key_code <= ui::VKEY_Z) {
    return true;
  }

  static constexpr auto kPunctuationKeys =
      base::MakeFixedFlatSet<ui::KeyboardCode>({
          ui::VKEY_OEM_1,
          ui::VKEY_OEM_PLUS,
          ui::VKEY_OEM_COMMA,
          ui::VKEY_OEM_MINUS,
          ui::VKEY_OEM_PERIOD,
          ui::VKEY_OEM_2,
          ui::VKEY_OEM_3,
          ui::VKEY_OEM_4,
          ui::VKEY_OEM_5,
          ui::VKEY_OEM_6,
          ui::VKEY_OEM_7,
          ui::VKEY_OEM_8,
          ui::VKEY_OEM_102,
          ui::VKEY_OEM_103,
          ui::VKEY_OEM_104,
      });
  return kPunctuationKeys.contains(key_code);
}

bool IsDigitKey(ui::KeyboardCode key_code) {
  return key_code >= ui::VKEY_0 && key_code <= ui::VKEY_9;
}

bool IsSixPackKey(ui::KeyboardCode key_code) {
  static constexpr auto kSixPackKeys = base::MakeFixedFlatSet<ui::KeyboardCode>(
      {ui::VKEY_PRIOR, ui::VKEY_NEXT, ui::VKEY_END, ui::VKEY_HOME,
       ui::VKEY_INSERT, ui::VKEY_DELETE});
  return kSixPackKeys.contains(key_code);
}

bool IsNumpadKey(ui::KeyboardCode key_code) {
  // Numpad keys are all in consecutive order.
  return key_code >= ui::VKEY_NUMPAD0 && key_code <= ui::VKEY_DIVIDE;
}

// This includes only the top row keys we know about, it is possible there are
// other on external keyboards. They would instead be considered misc keys.
bool IsTopRowKey(ui::KeyboardCode key_code, ui::DomCode dom_code) {
  static constexpr auto kTopRowKeys = base::MakeFixedFlatSet<ui::KeyboardCode>({
      ui::VKEY_F1,
      ui::VKEY_F2,
      ui::VKEY_F3,
      ui::VKEY_F4,
      ui::VKEY_F5,
      ui::VKEY_F6,
      ui::VKEY_F7,
      ui::VKEY_F8,
      ui::VKEY_F9,
      ui::VKEY_F10,
      ui::VKEY_F11,
      ui::VKEY_F12,
      ui::VKEY_F13,
      ui::VKEY_F14,
      ui::VKEY_F15,
      ui::VKEY_F16,
      ui::VKEY_F17,
      ui::VKEY_F18,
      ui::VKEY_F19,
      ui::VKEY_F20,
      ui::VKEY_F21,
      ui::VKEY_F22,
      ui::VKEY_F23,
      ui::VKEY_F24,
      ui::VKEY_BROWSER_BACK,
      ui::VKEY_BROWSER_FORWARD,
      ui::VKEY_BROWSER_REFRESH,
      ui::VKEY_BROWSER_STOP,
      ui::VKEY_BROWSER_SEARCH,
      ui::VKEY_BROWSER_FAVORITES,
      ui::VKEY_BROWSER_HOME,
      ui::VKEY_VOLUME_MUTE,
      ui::VKEY_VOLUME_DOWN,
      ui::VKEY_VOLUME_UP,
      ui::VKEY_MEDIA_NEXT_TRACK,
      ui::VKEY_MEDIA_PREV_TRACK,
      ui::VKEY_MEDIA_STOP,
      ui::VKEY_MEDIA_PLAY_PAUSE,
      ui::VKEY_MEDIA_LAUNCH_MAIL,
      ui::VKEY_MEDIA_LAUNCH_MEDIA_SELECT,
      ui::VKEY_MEDIA_LAUNCH_APP1,
      ui::VKEY_MEDIA_LAUNCH_APP2,
      ui::VKEY_PLAY,
      ui::VKEY_ZOOM,
      ui::VKEY_SNAPSHOT,
      ui::VKEY_PRIVACY_SCREEN_TOGGLE,
      ui::VKEY_MICROPHONE_MUTE_TOGGLE,
      ui::VKEY_BRIGHTNESS_DOWN,
      ui::VKEY_BRIGHTNESS_UP,
      ui::VKEY_KBD_BACKLIGHT_TOGGLE,
      ui::VKEY_KBD_BRIGHTNESS_DOWN,
      ui::VKEY_KBD_BRIGHTNESS_UP,
      ui::VKEY_SLEEP,
  });

  if (dom_code == ui::DomCode::SHOW_ALL_WINDOWS) {
    return true;
  }

  return kTopRowKeys.contains(key_code);
}

}  // namespace

std::optional<ash::KeyCodeLookupEntry> FindKeyCodeEntry(
    ui::KeyboardCode key_code,
    ui::DomCode original_dom_code,
    bool remap_positional_key) {
  std::optional<ash::KeyCodeLookupEntry> cached_key_data =
      ash::AcceleratorKeycodeLookupCache::Get()->Find(key_code,
                                                      remap_positional_key);
  // Cache hit, return immediately.
  if (cached_key_data) {
    return cached_key_data;
  }

  ui::DomKey dom_key;
  ui::KeyboardCode key_code_to_compare = ui::VKEY_UNKNOWN;
  const ui::KeyboardLayoutEngine* layout_engine =
      ui::KeyboardLayoutEngineManager::GetKeyboardLayoutEngine();

  // The input |key_code| is the |KeyboardCode| aka VKEY of the shortcut in
  // the US layout which is registered from the shortcut table. |key_code|
  // is first mapped to the |DomCode| this key is on in the US layout. If
  // the key is not positional, this processing is skipped and it is handled
  // normally in the loop below. For the positional keys, the |DomCode| is
  // then mapped to the |DomKey| in the current layout which represents the
  // glyph/character that appears on the key (and usually when typed).

  // Positional keys are direct lookups, no need to store in the cache.
  if (remap_positional_key &&
      ::features::IsImprovedKeyboardShortcutsEnabled()) {
    ui::DomCode dom_code =
        ui::KeycodeConverter::MapUSPositionalShortcutKeyToDomCode(
            key_code, original_dom_code);
    if (dom_code != ui::DomCode::NONE) {
      std::u16string result;
      if (IsValidDomCode(dom_code) &&
          layout_engine->Lookup(dom_code, /*event_flags=*/ui::EF_NONE, &dom_key,
                                &key_code_to_compare)) {
        if (!dom_key.IsValid()) {
          return std::nullopt;
        }
        if (dom_key.IsDeadKey()) {
          result = GetStringForDeadKey(dom_key);
        } else {
          result = base::UTF8ToUTF16(
              ui::KeycodeConverter::DomKeyToKeyString(dom_key));
        }
      }
      if (dom_key != ui::DomKey::UNIDENTIFIED) {
        ash::AcceleratorKeycodeLookupCache::Get()->InsertOrAssign(
            key_code, /*remap_positional_key=*/remap_positional_key, dom_code,
            dom_key, key_code_to_compare, result);
      }
      return KeyCodeLookupEntry{dom_code, dom_key, key_code_to_compare, result};
    }
  }

  // Cache miss, get the key string and store it.
  for (const auto& dom_code : ui::kDomCodesArray) {
    if (IsValidDomCode(dom_code) &&
        !layout_engine->Lookup(dom_code, /*event_flags=*/ui::EF_NONE, &dom_key,
                               &key_code_to_compare)) {
      continue;
    }

    if (!dom_key.IsValid() || dom_key == ui::DomKey::UNIDENTIFIED) {
      continue;
    }

    if (key_code_to_compare != key_code) {
      continue;
    }

    const std::u16string key_string =
        base::UTF8ToUTF16(ui::KeycodeConverter::DomKeyToKeyString(dom_key));
    if (dom_key != ui::DomKey::UNIDENTIFIED) {
      AcceleratorKeycodeLookupCache::Get()->InsertOrAssign(
          key_code,
          /*remap_positional_key=*/remap_positional_key, dom_code, dom_key,
          key_code_to_compare, key_string);
    }

    return ash::KeyCodeLookupEntry{dom_code, dom_key, key_code_to_compare,
                                   key_string};
  }
  return std::nullopt;
}

std::u16string KeycodeToKeyString(ui::KeyboardCode key_code,
                                  bool remap_positional_key) {
  auto entry =
      FindKeyCodeEntry(key_code, ui::DomCode::NONE, remap_positional_key);
  return entry ? std::move(entry->key_display) : std::u16string();
}

std::u16string GetKeyDisplay(ui::KeyboardCode key_code,
                             bool remap_positional_key) {
  // If there's an entry for this key_code in our
  // map, return that entry's value.
  auto it = GetKeyDisplayMap().find(key_code);
  if (it != GetKeyDisplayMap().end()) {
    return it->second;
  } else {
    const std::u16string unconverted_string =
        KeycodeToKeyString(key_code, remap_positional_key);
    const std::string converted_string = base::UTF16ToUTF8(unconverted_string);
    // If `KeycodeToKeyString` fails to get a proper string, fallback to
    // the domcode string.
    if (converted_string == kUnidentifiedKeyString || converted_string == "") {
      ui::DomCode converted_domcode =
          ui::UsLayoutKeyboardCodeToDomCode(key_code);
      if (converted_domcode != ui::DomCode::NONE) {
        return base::UTF8ToUTF16(
            ui::KeycodeConverter::DomCodeToCodeString(converted_domcode));
      }

      // If no DomCode can be mapped, attempt reverse DomKey mappings.
      for (const auto& domkey_it : ui::kDomKeyToKeyboardCodeMap) {
        if (domkey_it.key_code == key_code) {
          return base::UTF8ToUTF16(
              ui::KeycodeConverter::DomKeyToKeyString(domkey_it.dom_key));
        }
      }
      // Else, return "Key {digit}" for Unidentified key.
      return base::UTF8ToUTF16(
          base::StringPrintf("Key %u", static_cast<unsigned int>(key_code)));
    }
    return unconverted_string;
  }
}

AcceleratorKeyInputType GetKeyInputTypeFromKeyEvent(
    const ui::KeyEvent& key_event) {
  const ui::KeyboardCode key_code = key_event.key_code();
  if (IsAlphaOrPunctuationKey(key_code)) {
    return AcceleratorKeyInputType::kAlpha;
  }

  if (IsDigitKey(key_code)) {
    return AcceleratorKeyInputType::kDigit;
  }

  if (IsTopRowKey(key_code, key_event.code())) {
    return AcceleratorKeyInputType::kTopRow;
  }

  if (IsSixPackKey(key_code)) {
    return AcceleratorKeyInputType::kSixPack;
  }

  if (IsNumpadKey(key_code)) {
    return AcceleratorKeyInputType::kNumberPad;
  }

  if (HasRightAltProperty(key_event)) {
    return AcceleratorKeyInputType::kRightAlt;
  }

  switch (key_event.code()) {
    case ui::DomCode::META_LEFT:
      return AcceleratorKeyInputType::kMetaLeft;
    case ui::DomCode::META_RIGHT:
      return AcceleratorKeyInputType::kMetaRight;
    case ui::DomCode::CONTROL_LEFT:
      return AcceleratorKeyInputType::kControlLeft;
    case ui::DomCode::CONTROL_RIGHT:
      return AcceleratorKeyInputType::kControlRight;
    case ui::DomCode::ALT_LEFT:
      return AcceleratorKeyInputType::kAltLeft;
    case ui::DomCode::ALT_RIGHT:
      if (key_event.key_code() == ui::VKEY_ALTGR) {
        return AcceleratorKeyInputType::kAltGr;
      }
      return AcceleratorKeyInputType::kAltRight;
    case ui::DomCode::SHIFT_LEFT:
      return AcceleratorKeyInputType::kShiftLeft;
    case ui::DomCode::SHIFT_RIGHT:
      return AcceleratorKeyInputType::kShiftRight;
    case ui::DomCode::FN:
      return AcceleratorKeyInputType::kFunction;
    default:
      break;
  }

  switch (key_code) {
    case ui::VKEY_ESCAPE:
      return AcceleratorKeyInputType::kEscape;
    case ui::VKEY_TAB:
      return AcceleratorKeyInputType::kTab;
    case ui::VKEY_CAPITAL:
      return AcceleratorKeyInputType::kCapsLock;
    case ui::VKEY_SPACE:
      return AcceleratorKeyInputType::kSpace;
    case ui::VKEY_RETURN:
      return AcceleratorKeyInputType::kEnter;
    case ui::VKEY_BACK:
      return AcceleratorKeyInputType::kBackspace;
    case ui::VKEY_UP:
      return AcceleratorKeyInputType::kUpArrow;
    case ui::VKEY_DOWN:
      return AcceleratorKeyInputType::kDownArrow;
    case ui::VKEY_RIGHT:
      return AcceleratorKeyInputType::kRightArrow;
    case ui::VKEY_LEFT:
      return AcceleratorKeyInputType::kLeftArrow;
    case ui::VKEY_ASSISTANT:
      return AcceleratorKeyInputType::kAssistant;
    default:
      break;
  }

  return AcceleratorKeyInputType::kMisc;
}

}  // namespace ash