About Mac Hotkeys and Virtual Keycodes

This doc is useful if you need to:

  • Examine the definition of hotkeys or add new ones
  • Determine why a certain hotkey press on a given keyboard layout maps to a particular command
  • Generally understand the flow from hotkey press to execution

Hotkey Definitions

Many Mac apps define hotkeys in nib files. We configure them in code.

Menu Hotkeys

Hidden Hotkeys

  • global_keyboard_shortcuts_mac.mm sets up hotkeys that aren‘t associated with any menu item, such as ⌘1 which selects the current browser window’s first tab

System Hotkeys

  • System Hotkeys (AKA Apple Symbolic Hotkeys) are things like “Mission Control” (^↑) which appear in the Shortcuts sheet in the Desktop and Dock pane in System Settings
  • The SystemHotkeyMap maintains a table of the user's system-reserved hotkeys
  • We call SystemHotkeyMap::IsHotkeyReserved() to quickly check if an incoming hotkey is a system hotkey (and ignore it if so)
  • See Documenting com.apple.symbolichotkeys.plist for more information on the different system hotkeys and how they're stored

Hotkey Execution

Hotkeys populate Chrome‘s menus but we don’t use the normal AppKit machinery to trigger their commands. Websites can override most hotkeys, which means we want to sometimes defer first to the renderer before falling back on a command in the menus.

- [ChromeCommandDispatcherDelegate prePerformKeyEquivalent:window:]

  • Starts hotkey processing
  • Executes commands for hotkeys that can't be overridden such as File->New and Chrome->Quit as well as hotkeys registered by extensions
  • Passes unconsumed hotkeys to the RenderWidgetHostViewCocoa

- [RenderWidgetHostViewCocoa performKeyEquivalent:]

  • Receives hotkeys for processing in RenderWidgetHostViewCocoa
  • Returns NO for any system-reserved hotkeys per the SystemHotkeyMap
  • Sends the rest to -keyEvent:wasKeyEquivalent:

- [RenderWidgetHostViewCocoa keyEvent:wasKeyEquivalent:]

  • Forwards incoming hotkeys to the renderer via _hostHelper->ForwardKeyboardEventWithCommands(), which gives the website a chance to consume them
  • Tells Cocoa the hotkey was consumed even though it doesn't know for sure (renderer hotkey processing is asynchronous)

- [CommandDispatcher redispatchKeyEvent:]

  • Receives hotkeys that weren't consumed by the renderer
  • Redispatches hotkeys to the ChromeCommandDispatcherDelegate for final processing

- [ChromeCommandDispatcherDelegate postPerformKeyEquivalent:window:isRedispatch:]

  • Calls CommandForKeyEvent() to locate the NSMenuItem with the corresponding hotkey and executes its command

CommandForKeyEvent()

  • Calls cr_firesForKeyEquivalentEvent: on each menu item until it finds one with the same hotkey
  • For hidden hotkeys (i.e. not associated with a menu item), this function calls cr_firesForKeyEquivalentEvent: on a set of invisible NSMenuItems that have been assigned the hidden hotkeys

- [NSMenuItem cr_firesForKeyEquivalentEvent:]

  • Returns YES if the menu item‘s hotkey matches the event’s
  • For non-US keyboard layouts, uses the event's characters, charactersIgnoringModifiers, and virtual keyCode to decide if they match
    • For example, if the incoming event's keycode is for a number key (kVK_ANSI_1 through kVK_ANSI_0), this method matches the event to the hotkeys that select a window tab (⌘1, etc.), regardless of the actual characters or charactersIgnoringModifiers in the event

About Virtual Keycodes

  • Virtual keycodes identify physical keys on a keyboard, providing a hardware- and language-independent way of specifying keyboard keys
  • Keycodes with “ANSI” in the name correspond to key positions on an ANSI-standard US keyboard
    • For example, kVK_ANSI_A is the virtual keycode for the key next to ‘Caps Lock’ in the US keyboard layout
  • A key's virtual keycode does not change when a modifier key is pressed but the character it generates might
  • Chrome uses Windows VKEY virtual keycodes, which differ from the Mac's keycodes, throughout its platform-independent code
  • “Located” keyboard codes identify specific keys with the same meaning
    • For example, VKEY_LSHIFT and VKEY_RSHIFT are ‘located’ keyboard codes while VKEY_SHIFT is their non-located representation
    • Similar mappings exist for the Option, Control, and numeric keypad keys

Sample Bugs

When debugging new hotkey / keycode issues, the following fixed bugs may help suggest a place to start.

For RTL tabstrips, switching tabs with ⌘[ and ⌘] behaves opposite to expectations (crbug.com/672876)

The default hotkey assignments for ⌘[ and ⌘], which select the “next” and “previous” tabs, respectively, needed to be swapped for RTL users. These hotkey definitions live in AcceleratorsCocoa(), which sets up the mapping of Chrome commands to hotkeys. After checking for RTL, the code exchanged the hotkey assignments.

Chrome instantly quits on Command-Q despite ‘Warn Before Quitting’ in non-US keyboard layout (crbug.com/142944)

In many non-US keyboard mappings, [hotkeyEvent charactersIgnoringModifiers] returns a non-ASCII character while [hotkeyEvent characters] returns an ASCII character. In these situations, as a quick hack we use the ASCII character string to determine which hotkey the user is triggering. Some keys in some non-US keyboard mappings, however, return ASCII for both -characters and -charactersIgnoringModifiers. This is the case for the kVK_ANSI_Q key in the Hebrew layout. As a result, pressing ⌘Q in the layout resulted in a ⌘\ hotkey event. No code looks for ⌘, and the original event filtered down to a layer that processed the ⌘Q correctly, but without the “Warn Before Quitting” check.

The solution was to check for this special “Q” / "" combination in [NSMenuItem(ChromeAdditions) cr_firesForKeyEquivalentEvent:] and interpret the event as ⌘Q.

Some Shortcut keys do not work on Dvorak-Right-Handed Keyboard (crbug.com/1358823)

Chrome Mac interprets ⌘kVK_ANSI_1 through ⌘kVK_ANSI_0 as a tab switch hotkey. However, certain flavors of Dvorak don't generate numbers from the numbered ANSI keys. For example, kVK_ANSI_8 generates a “p”, so ⌘kVK_ANSI_8 should be interpreted as ⌘P (Print) but instead switched the browser to the eighth tab.

The solution was to add an exception for the Dvorak keyboards in [NSMenuItem(ChromeAdditions) cr_firesForKeyEquivalentEvent:].