1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/global_keyboard_shortcuts_mac.h"
7 #import <AppKit/AppKit.h>
8 #include <Carbon/Carbon.h>
10 #include "base/check.h"
11 #include "base/mac/foundation_util.h"
12 #include "base/no_destructor.h"
13 #include "base/stl_util.h"
14 #include "build/buildflag.h"
15 #include "chrome/app/chrome_command_ids.h"
16 #import "chrome/browser/app_controller_mac.h"
17 #include "chrome/browser/ui/cocoa/accelerators_cocoa.h"
18 #import "chrome/browser/ui/cocoa/nsmenuitem_additions.h"
19 #include "ui/base/accelerators/accelerator.h"
20 #include "ui/base/accelerators/platform_accelerator_cocoa.h"
21 #include "ui/events/event_constants.h"
22 #include "ui/events/keycodes/keyboard_code_conversion_mac.h"
26 // Returns a ui::Accelerator given a KeyboardShortcutData.
27 ui::Accelerator AcceleratorFromShortcut(const KeyboardShortcutData& shortcut) {
29 if (shortcut.command_key)
30 modifiers |= ui::EF_COMMAND_DOWN;
31 if (shortcut.shift_key)
32 modifiers |= ui::EF_SHIFT_DOWN;
33 if (shortcut.cntrl_key)
34 modifiers |= ui::EF_CONTROL_DOWN;
36 modifiers |= ui::EF_ALT_DOWN;
38 return ui::Accelerator(ui::KeyboardCodeFromKeyCode(shortcut.vkey_code),
42 // Returns the menu item associated with |key| in |menu|, or nil if not found.
43 NSMenuItem* FindMenuItem(NSEvent* key, NSMenu* menu) {
44 NSMenuItem* result = nil;
46 for (NSMenuItem* item in [menu itemArray]) {
47 NSMenu* submenu = [item submenu];
49 if (submenu != [NSApp servicesMenu])
50 result = FindMenuItem(key, submenu);
51 } else if ([item cr_firesForKeyEvent:key]) {
62 int MenuCommandForKeyEvent(NSEvent* event) {
63 if ([event type] != NSKeyDown)
66 // We avoid calling -[NSMenuDelegate menuNeedsUpdate:] on each submenu's
67 // delegate as that can be slow. Instead, we update the relevant NSMenuItems
68 // if [NSApp delegate] is an instance of AppController. See
69 // https://crbug.com/851260#c4.
70 [base::mac::ObjCCast<AppController>([NSApp delegate])
71 updateMenuItemKeyEquivalents];
73 // Then call -[NSMenu update], which will validate every user interface item.
74 [[NSApp mainMenu] update];
76 NSMenuItem* item = FindMenuItem(event, [NSApp mainMenu]);
81 if ([item action] == @selector(commandDispatch:) && [item tag] > 0)
84 // "Close window" doesn't use the |commandDispatch:| mechanism. Menu items
85 // that do not correspond to IDC_ constants need no special treatment however,
86 // as they can't be blacklisted in
87 // |BrowserCommandController::IsReservedCommandOrKey()| anyhow.
88 if ([item action] == @selector(performClose:))
89 return IDC_CLOSE_WINDOW;
91 // "Exit" doesn't use the |commandDispatch:| mechanism either.
92 if ([item action] == @selector(terminate:))
98 bool MatchesEventForKeyboardShortcut(const KeyboardShortcutData& shortcut,
104 return shortcut.command_key == command_key &&
105 shortcut.shift_key == shift_key && shortcut.cntrl_key == cntrl_key &&
106 shortcut.opt_key == opt_key && shortcut.vkey_code == vkey_code;
109 const std::vector<KeyboardShortcutData>&
110 GetDelayedShortcutsNotPresentInMainMenu() {
111 static base::NoDestructor<std::vector<KeyboardShortcutData>> keys({
112 // cmd shift cntrl option vkeycode command
113 //--- ----- ----- ------ -------- -------
114 {true, false, false, false, kVK_LeftArrow, IDC_BACK},
115 {true, false, false, false, kVK_RightArrow, IDC_FORWARD},
120 CommandForKeyEventResult NoCommand() {
121 return {-1, /*from_main_menu=*/false};
124 CommandForKeyEventResult MainMenuCommand(int cmd) {
125 return {cmd, /*from_main_menu=*/true};
128 CommandForKeyEventResult ShortcutCommand(int cmd) {
129 return {cmd, /*from_main_menu=*/false};
134 const std::vector<KeyboardShortcutData>& GetShortcutsNotPresentInMainMenu() {
136 static base::NoDestructor<std::vector<KeyboardShortcutData>> keys({
137 // cmd shift cntrl option vkeycode command
138 // --- ----- ----- ------ -------- -------
139 {true, true, false, false, kVK_ANSI_RightBracket, IDC_SELECT_NEXT_TAB},
140 {true, true, false, false, kVK_ANSI_LeftBracket, IDC_SELECT_PREVIOUS_TAB},
141 {false, false, true, false, kVK_PageDown, IDC_SELECT_NEXT_TAB},
142 {false, false, true, false, kVK_PageUp, IDC_SELECT_PREVIOUS_TAB},
143 {true, false, false, true, kVK_RightArrow, IDC_SELECT_NEXT_TAB},
144 {true, false, false, true, kVK_LeftArrow, IDC_SELECT_PREVIOUS_TAB},
146 // Cmd-0..8 select the nth tab, with cmd-9 being "last tab".
147 {true, false, false, false, kVK_ANSI_1, IDC_SELECT_TAB_0},
148 {true, false, false, false, kVK_ANSI_Keypad1, IDC_SELECT_TAB_0},
149 {true, false, false, false, kVK_ANSI_2, IDC_SELECT_TAB_1},
150 {true, false, false, false, kVK_ANSI_Keypad2, IDC_SELECT_TAB_1},
151 {true, false, false, false, kVK_ANSI_3, IDC_SELECT_TAB_2},
152 {true, false, false, false, kVK_ANSI_Keypad3, IDC_SELECT_TAB_2},
153 {true, false, false, false, kVK_ANSI_4, IDC_SELECT_TAB_3},
154 {true, false, false, false, kVK_ANSI_Keypad4, IDC_SELECT_TAB_3},
155 {true, false, false, false, kVK_ANSI_5, IDC_SELECT_TAB_4},
156 {true, false, false, false, kVK_ANSI_Keypad5, IDC_SELECT_TAB_4},
157 {true, false, false, false, kVK_ANSI_6, IDC_SELECT_TAB_5},
158 {true, false, false, false, kVK_ANSI_Keypad6, IDC_SELECT_TAB_5},
159 {true, false, false, false, kVK_ANSI_7, IDC_SELECT_TAB_6},
160 {true, false, false, false, kVK_ANSI_Keypad7, IDC_SELECT_TAB_6},
161 {true, false, false, false, kVK_ANSI_8, IDC_SELECT_TAB_7},
162 {true, false, false, false, kVK_ANSI_Keypad8, IDC_SELECT_TAB_7},
163 {true, false, false, false, kVK_ANSI_9, IDC_SELECT_LAST_TAB},
164 {true, false, false, false, kVK_ANSI_Keypad9, IDC_SELECT_LAST_TAB},
165 {true, true, false, false, kVK_ANSI_M, IDC_SHOW_AVATAR_MENU},
166 {true, false, false, true, kVK_ANSI_L, IDC_SHOW_DOWNLOADS},
167 {true, true, false, false, kVK_ANSI_C, IDC_DEV_TOOLS_INSPECT},
168 {true, false, false, true, kVK_ANSI_C, IDC_DEV_TOOLS_INSPECT},
170 {true, false, false, true, kVK_DownArrow, IDC_FOCUS_NEXT_PANE},
171 {true, false, false, true, kVK_UpArrow, IDC_FOCUS_PREVIOUS_PANE},
177 const std::vector<NSMenuItem*>& GetMenuItemsNotPresentInMainMenu() {
178 static base::NoDestructor<std::vector<NSMenuItem*>> menu_items;
179 if (menu_items->empty()) {
180 for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) {
181 ui::Accelerator accelerator = AcceleratorFromShortcut(shortcut);
182 NSString* key_equivalent = nil;
183 NSUInteger modifier_mask = 0;
184 ui::GetKeyEquivalentAndModifierMaskFromAccelerator(
185 accelerator, &key_equivalent, &modifier_mask);
187 // Intentionally leaked!
188 NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@""
190 keyEquivalent:key_equivalent];
191 item.keyEquivalentModifierMask = modifier_mask;
193 // We store the command in the tag.
194 item.tag = shortcut.chrome_command;
195 menu_items->push_back(item);
201 CommandForKeyEventResult CommandForKeyEvent(NSEvent* event) {
203 if ([event type] != NSKeyDown)
206 int cmdNum = MenuCommandForKeyEvent(event);
208 return MainMenuCommand(cmdNum);
210 // Scan through keycodes and see if it corresponds to one of the non-menu
212 for (NSMenuItem* menu_item : GetMenuItemsNotPresentInMainMenu()) {
213 if ([menu_item cr_firesForKeyEvent:event])
214 return ShortcutCommand(menu_item.tag);
220 int DelayedWebContentsCommandForKeyEvent(NSEvent* event) {
222 if ([event type] != NSKeyDown)
225 // Look in secondary keyboard shortcuts.
226 NSUInteger modifiers = [event modifierFlags];
227 const bool cmdKey = (modifiers & NSCommandKeyMask) != 0;
228 const bool shiftKey = (modifiers & NSShiftKeyMask) != 0;
229 const bool cntrlKey = (modifiers & NSControlKeyMask) != 0;
230 const bool optKey = (modifiers & NSAlternateKeyMask) != 0;
231 const int keyCode = [event keyCode];
233 // Scan through keycodes and see if it corresponds to one of the non-menu
235 for (const auto& shortcut : GetDelayedShortcutsNotPresentInMainMenu()) {
236 if (MatchesEventForKeyboardShortcut(shortcut, cmdKey, shiftKey, cntrlKey,
238 return shortcut.chrome_command;
245 // AppKit sends an event via performKeyEquivalent: if it has at least one of the
246 // command or control modifiers, and is an NSKeyDown event. CommandDispatcher
247 // supplements this by also sending event with the option modifier to
248 // performKeyEquivalent:.
249 bool EventUsesPerformKeyEquivalent(NSEvent* event) {
250 NSUInteger modifiers = [event modifierFlags];
251 if ((modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl |
252 NSEventModifierFlagOption)) == 0) {
255 return [event type] == NSKeyDown;
258 bool GetDefaultMacAcceleratorForCommandId(int command_id,
259 ui::Accelerator* accelerator) {
260 // See if it corresponds to one of the non-menu shortcuts.
261 for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) {
262 if (shortcut.chrome_command == command_id) {
263 *accelerator = AcceleratorFromShortcut(shortcut);
268 // See if it corresponds to one of the default NSMenu keyEquivalents.
269 const ui::Accelerator* default_nsmenu_equivalent =
270 AcceleratorsCocoa::GetInstance()->GetAcceleratorForCommand(command_id);
271 if (default_nsmenu_equivalent)
272 *accelerator = *default_nsmenu_equivalent;
273 return default_nsmenu_equivalent != nullptr;