// 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.
import '../strings.m.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {VKey as ash_mojom_VKey} from 'chrome://resources/ash/common/shortcut_input_ui/accelerator_keys.mojom-webui.js';
import {KeyEvent} from 'chrome://resources/ash/common/shortcut_input_ui/input_device_settings.mojom-webui.js';
import {ModifierKeyCodes} from 'chrome://resources/ash/common/shortcut_input_ui/shortcut_utils.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {mojoString16ToString} from 'chrome://resources/js/mojo_type_util.js';
import {Accelerator, AcceleratorCategory, AcceleratorConfigResult, AcceleratorId, AcceleratorInfo, AcceleratorKeyState, AcceleratorSource, AcceleratorState, AcceleratorSubcategory, AcceleratorType, Modifier, MojoAcceleratorInfo, MojoSearchResult, StandardAcceleratorInfo, TextAcceleratorInfo, TextAcceleratorPart} from './shortcut_types.js';
// TODO(jimmyxgong): ChromeOS currently supports up to F24 but can be updated to
// F32. Update here when F32 is available.
const kF11 = 112; // Keycode for F11.
const kF24 = 135; // Keycode for F24.
const kMeta = 91; // Keycode for Meta.
const modifiers: Modifier[] = [
Modifier.SHIFT,
Modifier.CONTROL,
Modifier.ALT,
Modifier.COMMAND,
Modifier.FN_KEY,
];
export const unidentifiedKeyCodeToKey: {[keyCode: number]: string} = {
159: 'MicrophoneMuteToggle',
192: '`', // Backquote key.
218: 'KeyboardBrightnessUp',
232: 'KeyboardBrightnessDown',
237: 'EmojiPicker',
238: 'EnableOrToggleDictation',
239: 'ViewAllApps',
};
// The keys in this map are pulled from the file:
// ui/events/keycodes/dom/dom_code_data.inc
export const keyToIconNameMap: {[key: string]: string|undefined} = {
'Accessibility': 'accessibility',
'ArrowDown': 'arrow-down',
'ArrowLeft': 'arrow-left',
'ArrowRight': 'arrow-right',
'ArrowUp': 'arrow-up',
'AudioVolumeDown': 'volume-down',
'AudioVolumeMute': 'volume-mute',
'AudioVolumeUp': 'volume-up',
'BrightnessDown': 'display-brightness-down',
'BrightnessUp': 'display-brightness-up',
'BrowserBack': 'back',
'BrowserForward': 'forward',
'BrowserHome': 'browser-home',
'BrowserRefresh': 'refresh',
'BrowserSearch': 'browser-search',
'ContextMenu': 'menu',
'EmojiPicker': 'emoji-picker',
'EnableOrToggleDictation': 'dictation-toggle',
'KeyboardBacklightToggle': 'keyboard-brightness-toggle',
'KeyboardBrightnessUp': 'keyboard-brightness-up',
'KeyboardBrightnessDown': 'keyboard-brightness-down',
'LaunchApplication1': 'overview',
'LaunchApplication2': 'calculator',
'LaunchAssistant': 'assistant',
'LaunchMail': 'launch-mail',
'MediaFastForward': 'fast-forward',
'MediaPause': 'pause',
'MediaPlay': 'play',
'MediaPlayPause': 'play-pause',
'MediaTrackNext': 'next-track',
'MediaTrackPrevious': 'last-track',
'MicrophoneMuteToggle': 'microphone-mute',
'ModeChange': 'globe',
'ViewAllApps': 'view-all-apps',
'Power': 'power',
'PrintScreen': 'screenshot',
'PrivacyScreenToggle': 'electronic-privacy-screen',
'Settings': 'settings',
'Standby': 'lock',
'ZoomToggle': 'fullscreen',
};
// Return true if shortcut customization is allowed.
export const isCustomizationAllowed = (): boolean => {
return loadTimeData.getBoolean('isCustomizationAllowed');
};
export const isTextAcceleratorInfo =
(accelInfo: AcceleratorInfo|MojoAcceleratorInfo):
accelInfo is TextAcceleratorInfo => {
return !!(accelInfo as TextAcceleratorInfo)
.layoutProperties.textAccelerator;
};
export const isStandardAcceleratorInfo =
(accelInfo: AcceleratorInfo|MojoAcceleratorInfo):
accelInfo is StandardAcceleratorInfo => {
return !!(accelInfo as StandardAcceleratorInfo)
.layoutProperties.standardAccelerator;
};
export const createEmptyAccelInfoFromAccel =
(accel: Accelerator): StandardAcceleratorInfo => {
return {
layoutProperties:
{standardAccelerator: {accelerator: accel, keyDisplay: ''}},
acceleratorLocked: false,
locked: false,
state: AcceleratorState.kEnabled,
type: AcceleratorType.kUser,
};
};
export const createEmptyAcceleratorInfo = (): StandardAcceleratorInfo => {
return createEmptyAccelInfoFromAccel(
{modifiers: 0, keyCode: 0, keyState: AcceleratorKeyState.PRESSED});
};
export const resetKeyEvent = (): KeyEvent => {
return {
vkey: ash_mojom_VKey.MIN_VALUE,
domCode: 0,
domKey: 0,
modifiers: 0,
keyDisplay: '',
};
};
export const getAcceleratorId =
(source: string|number, actionId: string|number): AcceleratorId => {
return `${source}-${actionId}`;
};
const categoryPrefix = 'category';
export const getCategoryNameStringId =
(category: AcceleratorCategory): string => {
switch (category) {
case AcceleratorCategory.kGeneral:
return `${categoryPrefix}General`;
case AcceleratorCategory.kDevice:
return `${categoryPrefix}Device`;
case AcceleratorCategory.kBrowser:
return `${categoryPrefix}Browser`;
case AcceleratorCategory.kText:
return `${categoryPrefix}Text`;
case AcceleratorCategory.kWindowsAndDesks:
return `${categoryPrefix}WindowsAndDesks`;
case AcceleratorCategory.kDebug:
return `${categoryPrefix}Debug`;
case AcceleratorCategory.kAccessibility:
return `${categoryPrefix}Accessibility`;
case AcceleratorCategory.kDebug:
return `${categoryPrefix}Debug`;
case AcceleratorCategory.kDeveloper:
return `${categoryPrefix}Developer`;
default: {
// If this case is reached, then an invalid category was passed in.
assertNotReached();
}
}
};
const subcategoryPrefix = 'subcategory';
export const getSubcategoryNameStringId =
(subcategory: AcceleratorSubcategory): string => {
switch (subcategory) {
case AcceleratorSubcategory.kGeneralControls:
return `${subcategoryPrefix}GeneralControls`;
case AcceleratorSubcategory.kApps:
return `${subcategoryPrefix}Apps`;
case AcceleratorSubcategory.kMedia:
return `${subcategoryPrefix}Media`;
case AcceleratorSubcategory.kInputs:
return `${subcategoryPrefix}Inputs`;
case AcceleratorSubcategory.kDisplay:
return `${subcategoryPrefix}Display`;
case AcceleratorSubcategory.kGeneral:
return `${subcategoryPrefix}General`;
case AcceleratorSubcategory.kBrowserNavigation:
return `${subcategoryPrefix}BrowserNavigation`;
case AcceleratorSubcategory.kPages:
return `${subcategoryPrefix}Pages`;
case AcceleratorSubcategory.kTabs:
return `${subcategoryPrefix}Tabs`;
case AcceleratorSubcategory.kBookmarks:
return `${subcategoryPrefix}Bookmarks`;
case AcceleratorSubcategory.kDeveloperTools:
return `${subcategoryPrefix}DeveloperTools`;
case AcceleratorSubcategory.kTextNavigation:
return `${subcategoryPrefix}TextNavigation`;
case AcceleratorSubcategory.kTextEditing:
return `${subcategoryPrefix}TextEditing`;
case AcceleratorSubcategory.kWindows:
return `${subcategoryPrefix}Windows`;
case AcceleratorSubcategory.kDesks:
return `${subcategoryPrefix}Desks`;
case AcceleratorSubcategory.kChromeVox:
return `${subcategoryPrefix}ChromeVox`;
case AcceleratorSubcategory.kMouseKeys:
return `${subcategoryPrefix}MouseKeys`;
case AcceleratorSubcategory.kVisibility:
return `${subcategoryPrefix}Visibility`;
case AcceleratorSubcategory.kAccessibilityNavigation:
return `${subcategoryPrefix}AccessibilityNavigation`;
default: {
// If this case is reached, then an invalid category was passed in.
assertNotReached();
}
}
};
export const getAccelerator =
(acceleratorInfo: StandardAcceleratorInfo): Accelerator => {
return acceleratorInfo.layoutProperties.standardAccelerator.accelerator;
};
export const areAcceleratorsEqual =
(first: Accelerator, second: Accelerator): boolean => {
return first.keyCode === second.keyCode &&
first.modifiers === second.modifiers &&
first.keyState === second.keyState;
};
/**
* Checks if a retry can bypass the last error. Returns true for
* kConflictCanOverride or kNonSearchAcceleratorWarning results.
*/
export const canBypassErrorWithRetry =
(result: AcceleratorConfigResult): boolean => {
return result === AcceleratorConfigResult.kConflictCanOverride ||
result === AcceleratorConfigResult.kNonSearchAcceleratorWarning;
};
/**
* Sort the modifiers in the order of ctrl, alt, shift, meta.
*/
export const getSortedModifiers = (modifierStrings: string[]): string[] => {
const sortOrder = ['meta', 'ctrl', 'alt', 'shift', 'fn'];
if (modifierStrings.length <= 1) {
return modifierStrings;
}
return modifierStrings.sort(
(a, b) => sortOrder.indexOf(a) - sortOrder.indexOf(b));
};
function getModifierCount(accelerator: Accelerator): number {
let count = 0;
for (const modifier of modifiers) {
if (accelerator.modifiers & modifier) {
++count;
}
}
return count;
}
function isSearchOnlyAccelerator(accelerator: Accelerator): boolean {
return accelerator.keyCode === kMeta &&
accelerator.modifiers === Modifier.NONE;
}
// Comparison function that checks the number of modifiers in an accelerator.
// Lower number of modifiers get higher priority.
// @returns a negative number if the first accelerator info should be higher in
// the list, a positive number if it should be lower, 0 if it should have the
// same position
export function compareAcceleratorInfos(
first: AcceleratorInfo, second: AcceleratorInfo): number {
// Ignore non-standard accelerator infos as they only have one entry and is
// a no-opt.
if (!isStandardAcceleratorInfo(first) || !isStandardAcceleratorInfo(second)) {
return 0;
}
// Search/meta as the activation key should always be the highest priority.
if (isSearchOnlyAccelerator(
first.layoutProperties.standardAccelerator.accelerator)) {
return -1;
}
if (isSearchOnlyAccelerator(
second.layoutProperties.standardAccelerator.accelerator)) {
return 1;
}
const firstModifierCount =
getModifierCount(first.layoutProperties.standardAccelerator.accelerator);
const secondModifierCount =
getModifierCount(second.layoutProperties.standardAccelerator.accelerator);
return firstModifierCount - secondModifierCount;
}
/**
* Returns the converted modifier flag as a readable string.
* TODO(jimmyxgong): Localize, replace with icon, or update strings.
*/
export function getModifierString(modifier: Modifier): string {
switch (modifier) {
case Modifier.SHIFT:
return 'shift';
case Modifier.CONTROL:
return 'ctrl';
case Modifier.ALT:
return 'alt';
case Modifier.FN_KEY:
return 'fn';
case Modifier.COMMAND:
return 'meta';
default:
assertNotReached();
}
}
/**
* @returns the list of modifier keys for the given AcceleratorInfo.
*/
export function getModifiersForAcceleratorInfo(
acceleratorInfo: StandardAcceleratorInfo): string[] {
const modifierStrings: string[] = [];
for (const modifier of modifiers) {
if ((getAccelerator(acceleratorInfo)).modifiers & modifier) {
modifierStrings.push(getModifierString(modifier));
}
}
return getSortedModifiers(modifierStrings);
}
export const SHORTCUTS_APP_URL = 'chrome://shortcut-customization';
export const META_KEY = 'meta';
export const LWIN_KEY = 'Meta';
export const getURLForSearchResult = (searchResult: MojoSearchResult): URL => {
const url = new URL(SHORTCUTS_APP_URL);
const {action, category} = searchResult.acceleratorLayoutInfo;
url.searchParams.append('action', action.toString());
url.searchParams.append('category', category.toString());
return url;
};
export const isFunctionKey = (keycode: number): boolean => {
return keycode >= kF11 && keycode <= kF24;
};
export const isModifierKey = (keycode: number): boolean => {
return ModifierKeyCodes.includes(keycode);
};
export const isValidAccelerator = (accelerator: Accelerator): boolean => {
// A valid default accelerator is one that has modifier(s) and a key or
// is function key.
return (accelerator.modifiers > 0 && accelerator.keyCode > 0) ||
isFunctionKey(accelerator.keyCode);
};
export const containsAccelerator =
(accelerators: Accelerator[], accelerator: Accelerator): boolean => {
return accelerators.some(
accel => areAcceleratorsEqual(accel, accelerator));
};
export const getSourceAndActionFromAcceleratorId =
(uuid: AcceleratorId): {source: number, action: number} => {
// Split '{source}-{action}` into [source][action].
const uuidSplit = uuid.split('-');
const source: AcceleratorSource = parseInt(uuidSplit[0], 10);
const action = parseInt(uuidSplit[1], 10);
return {source, action};
};
/**
*
* @param keyOrIcon the text for an individual accelerator key.
* @returns the associated icon label for the given `keyOrIcon` text if it
* exists, otherwise returns `keyOrIcon` itself.
*/
export const getKeyDisplay = (keyOrIcon: string): string => {
const iconName = keyToIconNameMap[keyOrIcon];
return iconName ? loadTimeData.getString(`iconLabel${keyOrIcon}`) : keyOrIcon;
};
/**
* Translate a numpadKey code to a display string.
*/
export const getNumpadKeyDisplay = (code: string): string => {
// For "NumpadEnter", it is the same as "enter" key.
if (code === 'NumpadEnter') {
return 'enter';
}
// Map of special numpad key codes to their display symbols.
const numpadKeyMap: {[code: string]: string} = {
'NumpadAdd': '+',
'NumpadDecimal': '.',
'NumpadDivide': '/',
'NumpadMultiply': '*',
'NumpadSubtract': '-',
};
// Return the formatted string, using the map for special keys,
// or stripping 'Numpad' for numeric keys.
const numpadKey = numpadKeyMap[code] || code.replace('Numpad', '');
return `numpad ${numpadKey}`.toLowerCase();
};
/**
* Translate an unidentified key to a display string.
*/
export const getUnidentifiedKeyDisplay = (e: KeyboardEvent): string => {
if (e.code === 'Backquote') {
// Backquote `key` will become 'unidentified' when ctrl
// is pressed.
if (e.ctrlKey) {
return unidentifiedKeyCodeToKey[e.keyCode];
}
return e.key;
}
if (e.code === '') {
// If there is no `code`, check the `key`. If the `key` is
// `unidentified`, we need to manually lookup the key.
return unidentifiedKeyCodeToKey[e.keyCode] || e.key;
}
return `Key ${e.keyCode}`;
};
/**
* @returns the Aria label for the standard accelerators.
*/
export const getAriaLabelForStandardAccelerators =
(acceleratorInfos: StandardAcceleratorInfo[], dividerString: string):
string => {
return acceleratorInfos
.map(
(acceleratorInfo: StandardAcceleratorInfo) =>
getAriaLabelForStandardAcceleratorInfo(acceleratorInfo))
.join(` ${dividerString} `);
};
/**
* @returns the Aria label for the text accelerators.
*/
export const getAriaLabelForTextAccelerators =
(acceleratorInfos: TextAcceleratorInfo[]): string => {
return getTextAcceleratorParts(acceleratorInfos as TextAcceleratorInfo[])
.map(part => getKeyDisplay(mojoString16ToString(part.text)))
.join('');
};
/**
* @returns the Aria label for the given StandardAcceleratorInfo.
*/
export const getAriaLabelForStandardAcceleratorInfo =
(acceleratorInfo: StandardAcceleratorInfo): string => {
const keyOrIcon =
acceleratorInfo.layoutProperties.standardAccelerator.keyDisplay;
return getModifiersForAcceleratorInfo(acceleratorInfo)
.join(' ')
.concat(` ${getKeyDisplay(keyOrIcon)}`);
};
/**
* @returns the text accelerator parts for the given TextAcceleratorInfo.
*/
export const getTextAcceleratorParts =
(infos: TextAcceleratorInfo[]): TextAcceleratorPart[] => {
// For text based layout accelerators, we always expect this to be an
// array with a single element.
assert(infos.length === 1);
const textAcceleratorInfo = infos[0];
assert(isTextAcceleratorInfo(textAcceleratorInfo));
return textAcceleratorInfo.layoutProperties.textAccelerator.parts;
};
export const getModifiersFromKeyboardEvent = (e: KeyboardEvent): Modifier => {
let modifiers = 0;
if (e.metaKey) {
modifiers |= Modifier.COMMAND;
}
if (e.ctrlKey) {
modifiers |= Modifier.CONTROL;
}
if (e.altKey) {
modifiers |= Modifier.ALT;
}
if (e.key == 'Shift' || e.shiftKey) {
modifiers |= Modifier.SHIFT;
}
return modifiers;
};
export const getKeyDisplayFromKeyboardEvent = (e: KeyboardEvent): string => {
// Handle numpad keys:
if (e.code.startsWith('Numpad')) {
return getNumpadKeyDisplay(e.code);
}
// Handle unidentified keys:
if (e.key === 'Unidentified' || e.code === '') {
return getUnidentifiedKeyDisplay(e);
}
switch (e.code) {
case 'Space': // Space key: e.key: ' ', e.code: 'Space', set keyDisplay
// to be 'space' text.
return 'space';
case 'ShowAllWindows': // Overview key: e.key: 'F4', e.code:
// 'ShowAllWindows', set keyDisplay to be
// 'LaunchApplication1' and will display as
// 'overview' icon.
return 'LaunchApplication1';
default: // All other keys: Use the original e.key as keyDisplay.
return e.key;
}
};
export const keyEventToAccelerator = (keyEvent: KeyEvent): Accelerator => {
const output: Accelerator = {
modifiers: 0,
keyCode: 0,
keyState: AcceleratorKeyState.PRESSED,
};
output.modifiers = keyEvent.modifiers;
if (!isModifierKey(keyEvent.vkey) || isFunctionKey(keyEvent.vkey)) {
output.keyCode = keyEvent.vkey;
}
return output;
};