[email protected] | 0996e9b | 2011-08-26 17:59:01 | [diff] [blame] | 1 | // Copyright (c) 2011 The Chromium Authors. All rights reserved. |
[email protected] | 3641da6c | 2009-07-08 14:59:06 | [diff] [blame] | 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "chrome/browser/global_keyboard_shortcuts_mac.h" |
| 6 | |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 7 | #import <AppKit/AppKit.h> |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 8 | #include <Carbon/Carbon.h> |
jackhou | 06c25a9 | 2015-07-30 03:11:18 | [diff] [blame] | 9 | |
[email protected] | 92f5adfa | 2010-01-05 09:49:12 | [diff] [blame] | 10 | #include "base/logging.h" |
erikchen | 22be209 | 2018-06-12 15:54:00 | [diff] [blame] | 11 | #include "base/mac/foundation_util.h" |
Elly Fong-Jones | 58ccd17 | 2018-03-22 17:40:51 | [diff] [blame] | 12 | #include "base/no_destructor.h" |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 13 | #include "base/stl_util.h" |
Elly Fong-Jones | 58ccd17 | 2018-03-22 17:40:51 | [diff] [blame] | 14 | #include "build/buildflag.h" |
[email protected] | 1a3aba8 | 2010-11-08 23:52:54 | [diff] [blame] | 15 | #include "chrome/app/chrome_command_ids.h" |
erikchen | 22be209 | 2018-06-12 15:54:00 | [diff] [blame] | 16 | #import "chrome/browser/app_controller_mac.h" |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 17 | #include "chrome/browser/ui/cocoa/accelerators_cocoa.h" |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 18 | #import "chrome/browser/ui/cocoa/nsmenuitem_additions.h" |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 19 | #include "ui/base/accelerators/accelerator.h" |
erikchen | 008ab23a | 2018-08-09 02:06:39 | [diff] [blame] | 20 | #include "ui/base/accelerators/platform_accelerator_cocoa.h" |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 21 | #include "ui/events/event_constants.h" |
| 22 | #include "ui/events/keycodes/keyboard_code_conversion_mac.h" |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 23 | |
| 24 | namespace { |
| 25 | |
erikchen | 008ab23a | 2018-08-09 02:06:39 | [diff] [blame] | 26 | // Returns a ui::Accelerator given a KeyboardShortcutData. |
| 27 | ui::Accelerator AcceleratorFromShortcut(const KeyboardShortcutData& shortcut) { |
| 28 | int modifiers = 0; |
| 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; |
| 35 | if (shortcut.opt_key) |
| 36 | modifiers |= ui::EF_ALT_DOWN; |
| 37 | |
| 38 | return ui::Accelerator(ui::KeyboardCodeFromKeyCode(shortcut.vkey_code), |
| 39 | modifiers); |
| 40 | } |
| 41 | |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 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; |
| 45 | |
| 46 | for (NSMenuItem* item in [menu itemArray]) { |
| 47 | NSMenu* submenu = [item submenu]; |
| 48 | if (submenu) { |
| 49 | if (submenu != [NSApp servicesMenu]) |
| 50 | result = FindMenuItem(key, submenu); |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 51 | } else if ([item cr_firesForKeyEvent:key]) { |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 52 | result = item; |
| 53 | } |
| 54 | |
| 55 | if (result) |
| 56 | break; |
| 57 | } |
| 58 | |
| 59 | return result; |
| 60 | } |
| 61 | |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 62 | int MenuCommandForKeyEvent(NSEvent* event) { |
| 63 | if ([event type] != NSKeyDown) |
| 64 | return -1; |
| 65 | |
erikchen | 22be209 | 2018-06-12 15:54:00 | [diff] [blame] | 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]; |
| 72 | |
| 73 | // Then call -[NSMenu update], which will validate every user interface item. |
| 74 | [[NSApp mainMenu] update]; |
| 75 | |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 76 | NSMenuItem* item = FindMenuItem(event, [NSApp mainMenu]); |
| 77 | |
| 78 | if (!item) |
| 79 | return -1; |
| 80 | |
| 81 | if ([item action] == @selector(commandDispatch:) && [item tag] > 0) |
| 82 | return [item tag]; |
| 83 | |
| 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; |
| 90 | |
| 91 | // "Exit" doesn't use the |commandDispatch:| mechanism either. |
| 92 | if ([item action] == @selector(terminate:)) |
| 93 | return IDC_EXIT; |
| 94 | |
| 95 | return -1; |
| 96 | } |
| 97 | |
mblsha | cb9c6b9d | 2016-11-21 17:11:18 | [diff] [blame] | 98 | bool MatchesEventForKeyboardShortcut(const KeyboardShortcutData& shortcut, |
| 99 | bool command_key, |
| 100 | bool shift_key, |
| 101 | bool cntrl_key, |
| 102 | bool opt_key, |
Erik Chen | e4ab96f | 2018-06-09 04:21:11 | [diff] [blame] | 103 | int vkey_code) { |
| 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; |
[email protected] | 92f5adfa | 2010-01-05 09:49:12 | [diff] [blame] | 107 | } |
| 108 | |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 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}, |
| 116 | }); |
| 117 | return *keys; |
| 118 | } |
| 119 | |
erikchen | 41281cd | 2018-06-20 18:15:05 | [diff] [blame] | 120 | CommandForKeyEventResult NoCommand() { |
| 121 | return {-1, /*from_main_menu=*/false}; |
| 122 | } |
| 123 | |
| 124 | CommandForKeyEventResult MainMenuCommand(int cmd) { |
| 125 | return {cmd, /*from_main_menu=*/true}; |
| 126 | } |
| 127 | |
| 128 | CommandForKeyEventResult ShortcutCommand(int cmd) { |
| 129 | return {cmd, /*from_main_menu=*/false}; |
| 130 | } |
| 131 | |
mblsha | cb9c6b9d | 2016-11-21 17:11:18 | [diff] [blame] | 132 | } // namespace |
| 133 | |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 134 | const std::vector<KeyboardShortcutData>& GetShortcutsNotPresentInMainMenu() { |
Elly Fong-Jones | 58ccd17 | 2018-03-22 17:40:51 | [diff] [blame] | 135 | // clang-format off |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 136 | static base::NoDestructor<std::vector<KeyboardShortcutData>> keys({ |
Erik Chen | e4ab96f | 2018-06-09 04:21:11 | [diff] [blame] | 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}, |
Erik Chen | e4ab96f | 2018-06-09 04:21:11 | [diff] [blame] | 142 | {false, false, true, false, kVK_PageUp, IDC_SELECT_PREVIOUS_TAB}, |
erikchen | 3e0fccc | 2018-07-12 17:52:19 | [diff] [blame] | 143 | {true, false, false, true, kVK_RightArrow, IDC_SELECT_NEXT_TAB}, |
| 144 | {true, false, false, true, kVK_LeftArrow, IDC_SELECT_PREVIOUS_TAB}, |
Elly Fong-Jones | 58ccd17 | 2018-03-22 17:40:51 | [diff] [blame] | 145 | |
Elly Fong-Jones | 58ccd17 | 2018-03-22 17:40:51 | [diff] [blame] | 146 | // Cmd-0..8 select the nth tab, with cmd-9 being "last tab". |
Erik Chen | e4ab96f | 2018-06-09 04:21:11 | [diff] [blame] | 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}, |
Erik Chen | e4ab96f | 2018-06-09 04:21:11 | [diff] [blame] | 167 | {true, true, false, false, kVK_ANSI_C, IDC_DEV_TOOLS_INSPECT}, |
erikchen | 7ee15a50 | 2018-09-06 22:06:29 | [diff] [blame] | 168 | {true, false, false, true, kVK_ANSI_C, IDC_DEV_TOOLS_INSPECT}, |
Elly Fong-Jones | 58ccd17 | 2018-03-22 17:40:51 | [diff] [blame] | 169 | }); |
| 170 | // clang-format on |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 171 | return *keys; |
[email protected] | 92f5adfa | 2010-01-05 09:49:12 | [diff] [blame] | 172 | } |
| 173 | |
erikchen | 008ab23a | 2018-08-09 02:06:39 | [diff] [blame] | 174 | const std::vector<NSMenuItem*>& GetMenuItemsNotPresentInMainMenu() { |
| 175 | static base::NoDestructor<std::vector<NSMenuItem*>> menu_items; |
| 176 | if (menu_items->empty()) { |
| 177 | for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) { |
| 178 | ui::Accelerator accelerator = AcceleratorFromShortcut(shortcut); |
| 179 | NSString* key_equivalent = nil; |
| 180 | NSUInteger modifier_mask = 0; |
| 181 | ui::GetKeyEquivalentAndModifierMaskFromAccelerator( |
| 182 | accelerator, &key_equivalent, &modifier_mask); |
| 183 | |
| 184 | // Intentionally leaked! |
| 185 | NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:@"" |
| 186 | action:NULL |
| 187 | keyEquivalent:key_equivalent]; |
| 188 | item.keyEquivalentModifierMask = modifier_mask; |
| 189 | |
| 190 | // We store the command in the tag. |
| 191 | item.tag = shortcut.chrome_command; |
| 192 | menu_items->push_back(item); |
| 193 | } |
| 194 | } |
| 195 | return *menu_items; |
| 196 | } |
| 197 | |
erikchen | 41281cd | 2018-06-20 18:15:05 | [diff] [blame] | 198 | CommandForKeyEventResult CommandForKeyEvent(NSEvent* event) { |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 199 | DCHECK(event); |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 200 | if ([event type] != NSKeyDown) |
erikchen | 41281cd | 2018-06-20 18:15:05 | [diff] [blame] | 201 | return NoCommand(); |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 202 | |
Elly Fong-Jones | 1320aeb7 | 2017-10-13 20:30:37 | [diff] [blame] | 203 | int cmdNum = MenuCommandForKeyEvent(event); |
| 204 | if (cmdNum != -1) |
erikchen | 41281cd | 2018-06-20 18:15:05 | [diff] [blame] | 205 | return MainMenuCommand(cmdNum); |
andresantoso | 2389f84 | 2015-03-31 23:26:36 | [diff] [blame] | 206 | |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 207 | // Scan through keycodes and see if it corresponds to one of the non-menu |
| 208 | // shortcuts. |
erikchen | 008ab23a | 2018-08-09 02:06:39 | [diff] [blame] | 209 | for (NSMenuItem* menu_item : GetMenuItemsNotPresentInMainMenu()) { |
| 210 | if ([menu_item cr_firesForKeyEvent:event]) |
| 211 | return ShortcutCommand(menu_item.tag); |
[email protected] | 92f5adfa | 2010-01-05 09:49:12 | [diff] [blame] | 212 | } |
| 213 | |
erikchen | 41281cd | 2018-06-20 18:15:05 | [diff] [blame] | 214 | return NoCommand(); |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 215 | } |
| 216 | |
erikchen | 4c9d084 | 2018-06-14 17:04:08 | [diff] [blame] | 217 | int DelayedWebContentsCommandForKeyEvent(NSEvent* event) { |
| 218 | DCHECK(event); |
| 219 | if ([event type] != NSKeyDown) |
| 220 | return -1; |
| 221 | |
| 222 | // Look in secondary keyboard shortcuts. |
| 223 | NSUInteger modifiers = [event modifierFlags]; |
| 224 | const bool cmdKey = (modifiers & NSCommandKeyMask) != 0; |
| 225 | const bool shiftKey = (modifiers & NSShiftKeyMask) != 0; |
| 226 | const bool cntrlKey = (modifiers & NSControlKeyMask) != 0; |
| 227 | const bool optKey = (modifiers & NSAlternateKeyMask) != 0; |
| 228 | const int keyCode = [event keyCode]; |
| 229 | |
| 230 | // Scan through keycodes and see if it corresponds to one of the non-menu |
| 231 | // shortcuts. |
| 232 | for (const auto& shortcut : GetDelayedShortcutsNotPresentInMainMenu()) { |
| 233 | if (MatchesEventForKeyboardShortcut(shortcut, cmdKey, shiftKey, cntrlKey, |
| 234 | optKey, keyCode)) { |
| 235 | return shortcut.chrome_command; |
| 236 | } |
| 237 | } |
| 238 | |
| 239 | return -1; |
| 240 | } |
| 241 | |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 242 | // AppKit sends an event via performKeyEquivalent: if it has at least one of the |
| 243 | // command or control modifiers, and is an NSKeyDown event. CommandDispatcher |
| 244 | // supplements this by also sending event with the option modifier to |
| 245 | // performKeyEquivalent:. |
| 246 | bool EventUsesPerformKeyEquivalent(NSEvent* event) { |
| 247 | NSUInteger modifiers = [event modifierFlags]; |
| 248 | if ((modifiers & (NSEventModifierFlagCommand | NSEventModifierFlagControl | |
| 249 | NSEventModifierFlagOption)) == 0) { |
| 250 | return false; |
| 251 | } |
| 252 | return [event type] == NSKeyDown; |
| 253 | } |
| 254 | |
| 255 | bool GetDefaultMacAcceleratorForCommandId(int command_id, |
| 256 | ui::Accelerator* accelerator) { |
| 257 | // See if it corresponds to one of the non-menu shortcuts. |
| 258 | for (const auto& shortcut : GetShortcutsNotPresentInMainMenu()) { |
Erik Chen | e4ab96f | 2018-06-09 04:21:11 | [diff] [blame] | 259 | if (shortcut.chrome_command == command_id) { |
erikchen | 008ab23a | 2018-08-09 02:06:39 | [diff] [blame] | 260 | *accelerator = AcceleratorFromShortcut(shortcut); |
Erik Chen | 8f2f9568 | 2018-06-08 00:44:54 | [diff] [blame] | 261 | return true; |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | // See if it corresponds to one of the default NSMenu keyEquivalents. |
| 266 | const ui::Accelerator* default_nsmenu_equivalent = |
| 267 | AcceleratorsCocoa::GetInstance()->GetAcceleratorForCommand(command_id); |
| 268 | if (default_nsmenu_equivalent) |
| 269 | *accelerator = *default_nsmenu_equivalent; |
| 270 | return default_nsmenu_equivalent != nullptr; |
[email protected] | 1d313b83 | 2009-10-09 01:26:20 | [diff] [blame] | 271 | } |