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/feature_list.h"
12 #include "base/mac/foundation_util.h"
13 #include "base/no_destructor.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 #include "chrome/browser/ui/ui_features.h"
19 #include "ui/base/accelerators/accelerator.h"
20 #include "ui/base/accelerators/platform_accelerator_cocoa.h"
21 #import "ui/base/cocoa/nsmenuitem_additions.h"
22 #include "ui/base/ui_base_features.h"
23 #include "ui/events/event_constants.h"
24 #include "ui/events/keycodes/keyboard_code_conversion_mac.h"
28 // Returns a ui::Accelerator given a KeyboardShortcutData.
29 ui::Accelerator AcceleratorFromShortcut(const KeyboardShortcutData& shortcut) {
31 if (shortcut.command_key)
32 modifiers |= ui::EF_COMMAND_DOWN;
33 if (shortcut.shift_key)
34 modifiers |= ui::EF_SHIFT_DOWN;
35 if (shortcut.cntrl_key)
36 modifiers |= ui::EF_CONTROL_DOWN;
38 modifiers |= ui::EF_ALT_DOWN;
40 return ui::Accelerator(ui::KeyboardCodeFromKeyCode(shortcut.vkey_code),
44 // Returns the menu item associated with |key| in |menu|, or nil if not found.
45 NSMenuItem* FindMenuItem(NSEvent* key, NSMenu* menu) {
46 NSMenuItem* result = nil;
48 for (NSMenuItem* item in [menu itemArray]) {
49 NSMenu* submenu = [item submenu];
51 if (submenu != [NSApp servicesMenu])
52 result = FindMenuItem(key, submenu);
53 } else if ([item cr_firesForKeyEvent:key]) {
64 int MenuCommandForKeyEvent(NSEvent* event) {
65 if ([event type] != NSKeyDown)
68 // We avoid calling -[NSMenuDelegate menuNeedsUpdate:] on each submenu's
69 // delegate as that can be slow. Instead, we update the relevant NSMenuItems
70 // if [NSApp delegate] is an instance of AppController. See
71 // https://crbug.com/851260#c4.
72 [base::mac::ObjCCast<AppController>([NSApp delegate])
73 updateMenuItemKeyEquivalents];
75 // Then call -[NSMenu update], which will validate every user interface item.
76 [[NSApp mainMenu] update];
78 NSMenuItem* item = FindMenuItem(event, [NSApp mainMenu]);
83 if ([item action] == @selector(commandDispatch:) && [item tag] > 0)
86 // "Close window" doesn't use the |commandDispatch:| mechanism. Menu items
87 // that do not correspond to IDC_ constants need no special treatment however,
88 // as they can't be blacklisted in
89 // |BrowserCommandController::IsReservedCommandOrKey()| anyhow.
90 if ([item action] == @selector(performClose:))
91 return IDC_CLOSE_WINDOW;
93 // "Exit" doesn't use the |commandDispatch:| mechanism either.
94 if ([item action] == @selector(terminate:))
100 bool MatchesEventForKeyboardShortcut(const KeyboardShortcutData& shortcut,
106 return shortcut.command_key == command_key &&
107 shortcut.shift_key == shift_key && shortcut.cntrl_key == cntrl_key &&
108 shortcut.opt_key == opt_key && shortcut.vkey_code == vkey_code;
111 const std::vector<KeyboardShortcutData>&
112 GetDelayedShortcutsNotPresentInMainMenu() {
113 static base::NoDestructor<std::vector<KeyboardShortcutData>> keys({
114 // cmd shift cntrl option vkeycode command
115 //--- ----- ----- ------ -------- -------
116 {true, false, false, false, kVK_LeftArrow, IDC_BACK},
117 {true, false, false, false, kVK_RightArrow, IDC_FORWARD},
122 CommandForKeyEventResult NoCommand() {
123 return {-1, /*from_main_menu=*/false};
126 CommandForKeyEventResult MainMenuCommand(int cmd) {
127 return {cmd, /*from_main_menu=*/true};
130 CommandForKeyEventResult ShortcutCommand(int cmd) {
131 return {cmd, /*from_main_menu=*/false};
136 const std::vector<KeyboardShortcutData>& GetShortcutsNotPresentInMainMenu() {
138 static base::NoDestructor<std::vector<KeyboardShortcutData>> keys({
139 // cmd shift cntrl option vkeycode command
140 // --- ----- ----- ------ -------- -------
141 {true, true, false, false, kVK_ANSI_RightBracket, IDC_SELECT_NEXT_TAB},
142 {true, true, false, false, kVK_ANSI_LeftBracket, IDC_SELECT_PREVIOUS_TAB},
143 {false, false, true, false, kVK_PageDown, IDC_SELECT_NEXT_TAB},
144 {false, false, true, false, kVK_PageUp, IDC_SELECT_PREVIOUS_TAB},
145 {true, false, false, true, kVK_RightArrow, IDC_SELECT_NEXT_TAB},
146 {true, false, false, true, kVK_LeftArrow, IDC_SELECT_PREVIOUS_TAB},
148 // Cmd-0..8 select the nth tab, with cmd-9 being "last tab".
149 {true, false, false, false, kVK_ANSI_1, IDC_SELECT_TAB_0},
150 {true, false, false, false, kVK_ANSI_Keypad1, IDC_SELECT_TAB_0},
151 {true, false, false, false, kVK_ANSI_2, IDC_SELECT_TAB_1},
152 {true, false, false, false, kVK_ANSI_Keypad2, IDC_SELECT_TAB_1},
153 {true, false, false, false, kVK_ANSI_3, IDC_SELECT_TAB_2},
154 {true, false, false, false, kVK_ANSI_Keypad3, IDC_SELECT_TAB_2},
155 {true, false, false, false, kVK_ANSI_4, IDC_SELECT_TAB_3},
156 {true, false, false, false, kVK_ANSI_Keypad4, IDC_SELECT_TAB_3},
157 {true, false, false, false, kVK_ANSI_5, IDC_SELECT_TAB_4},
158 {true, false, false, false, kVK_ANSI_Keypad5, IDC_SELECT_TAB_4},
159 {true, false, false, false, kVK_ANSI_6, IDC_SELECT_TAB_5},
160 {true, false, false, false, kVK_ANSI_Keypad6, IDC_SELECT_TAB_5},
161 {true, false, false, false, kVK_ANSI_7, IDC_SELECT_TAB_6},
162 {true, false, false, false, kVK_ANSI_Keypad7, IDC_SELECT_TAB_6},
163 {true, false, false, false, kVK_ANSI_8, IDC_SELECT_TAB_7},
164 {true, false, false, false, kVK_ANSI_Keypad8, IDC_SELECT_TAB_7},
165 {true, false, false, false, kVK_ANSI_9, IDC_SELECT_LAST_TAB},
166 {true, false, false, false, kVK_ANSI_Keypad9, IDC_SELECT_LAST_TAB},
167 {true, true, false, false, kVK_ANSI_M, IDC_SHOW_AVATAR_MENU},
168 {true, false, false, true, kVK_ANSI_L, IDC_SHOW_DOWNLOADS},
169 {true, true, false, false, kVK_ANSI_C, IDC_DEV_TOOLS_INSPECT},
170 {true, false, false, true, kVK_ANSI_C, IDC_DEV_TOOLS_INSPECT},
172 {true, false, false, true, kVK_DownArrow, IDC_FOCUS_NEXT_PANE},
173 {true, false, false, true, kVK_UpArrow, IDC_FOCUS_PREVIOUS_PANE},
176 keys->push_back({true, true, false, false, kVK_ANSI_A, IDC_TAB_SEARCH});
178 if (base::FeatureList::IsEnabled(features::kUIDebugTools)) {
179 keys->push_back({false, true, true, true, kVK_ANSI_T,
180 IDC_DEBUG_TOGGLE_TABLET_MODE});
181 keys->push_back({false, true, true, true, kVK_ANSI_V,
182 IDC_DEBUG_PRINT_VIEW_TREE});
183 keys->push_back({false, true, true, true, kVK_ANSI_M,
184 IDC_DEBUG_PRINT_VIEW_TREE_DETAILS});
190 const std::vector<NSMenuItem*>& GetMenuItemsNotPresentInMainMenu() {
191 static base::NoDestructor<std::vector<NSMenuItem*>> menu_items;
192 if (menu_items->empty()) {
193 for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) {
194 ui::Accelerator accelerator = AcceleratorFromShortcut(shortcut);
195 NSString* key_equivalent = nil;
196 NSUInteger modifier_mask = 0;
197 ui::GetKeyEquivalentAndModifierMaskFromAccelerator(
198 accelerator, &key_equivalent, &modifier_mask);
200 // Intentionally leaked!
201 NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@""
203 keyEquivalent:key_equivalent];
204 item.keyEquivalentModifierMask = modifier_mask;
206 // We store the command in the tag.
207 item.tag = shortcut.chrome_command;
208 menu_items->push_back(item);
214 CommandForKeyEventResult CommandForKeyEvent(NSEvent* event) {
216 if ([event type] != NSKeyDown)
219 int cmdNum = MenuCommandForKeyEvent(event);
221 return MainMenuCommand(cmdNum);
223 // Scan through keycodes and see if it corresponds to one of the non-menu
225 for (NSMenuItem* menu_item : GetMenuItemsNotPresentInMainMenu()) {
226 if ([menu_item cr_firesForKeyEvent:event])
227 return ShortcutCommand(menu_item.tag);
233 int DelayedWebContentsCommandForKeyEvent(NSEvent* event) {
235 if ([event type] != NSKeyDown)
238 // Look in secondary keyboard shortcuts.
239 NSUInteger modifiers = [event modifierFlags];
240 const bool cmdKey = (modifiers & NSCommandKeyMask) != 0;
241 const bool shiftKey = (modifiers & NSShiftKeyMask) != 0;
242 const bool cntrlKey = (modifiers & NSControlKeyMask) != 0;
243 const bool optKey = (modifiers & NSAlternateKeyMask) != 0;
244 const int keyCode = [event keyCode];
246 // Scan through keycodes and see if it corresponds to one of the non-menu
248 for (const auto& shortcut : GetDelayedShortcutsNotPresentInMainMenu()) {
249 if (MatchesEventForKeyboardShortcut(shortcut, cmdKey, shiftKey, cntrlKey,
251 return shortcut.chrome_command;
258 // AppKit sends an event via performKeyEquivalent: if it has at least one of the
259 // command or control modifiers, and is an NSKeyDown event. CommandDispatcher
260 // supplements this by also sending event with the option modifier to
261 // performKeyEquivalent:.
262 bool EventUsesPerformKeyEquivalent(NSEvent* event) {
263 NSUInteger modifiers = [event modifierFlags];
264 if ((modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl |
265 NSEventModifierFlagOption)) == 0) {
268 return [event type] == NSKeyDown;
271 bool GetDefaultMacAcceleratorForCommandId(int command_id,
272 ui::Accelerator* accelerator) {
273 // See if it corresponds to one of the non-menu shortcuts.
274 for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) {
275 if (shortcut.chrome_command == command_id) {
276 *accelerator = AcceleratorFromShortcut(shortcut);
281 // See if it corresponds to one of the default NSMenu keyEquivalents.
282 const ui::Accelerator* default_nsmenu_equivalent =
283 AcceleratorsCocoa::GetInstance()->GetAcceleratorForCommand(command_id);
284 if (default_nsmenu_equivalent)
285 *accelerator = *default_nsmenu_equivalent;
286 return default_nsmenu_equivalent != nullptr;