// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import <Cocoa/Cocoa.h>
#include "base/i18n/base_i18n_switches.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/ui/cocoa/accelerators_cocoa.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "testing/gtest_mac.h"
#include "ui/base/accelerators/platform_accelerator_cocoa.h"
#include "ui/base/l10n/l10n_util_mac.h"
#import "ui/events/keycodes/keyboard_code_conversion_mac.h"
using AcceleratorsCocoaBrowserTest = InProcessBrowserTest;
namespace {
// Adds all NSMenuItems with an accelerator to the array.
void AddAcceleratorItemsToArray(NSMenu* menu, NSMutableArray* array) {
for (NSMenuItem* item in menu.itemArray) {
NSMenu* submenu = item.submenu;
if (submenu)
AddAcceleratorItemsToArray(submenu, array);
// If the tag or key equivalent is zero, then either this is a macOS menu
// item that we don't care about, or it's a chrome accelerator with non
// standard selector. We don't have an easy way to distinguish between
// these, so we just ignore them. Also as of macOS Monterey the AppKit
// adds a tag to the Start Dictation... menu item - skip it as well.
if (item.tag == 0 || item.keyEquivalent.length == 0 ||
item.action == @selector(startDictation:))
continue;
[array addObject:item];
}
}
// Checks that the |item|'s modifier mask matches |modifierMask|. The
// NSEventModifierFlagShift may be stored as part of the key equivalent
// (i.e. "a" + NSEventModifierFlagShift => "A").
inline bool MenuItemHasModifierMask(NSMenuItem* item, NSUInteger modifierMask) {
return modifierMask == item.keyEquivalentModifierMask ||
(modifierMask & ~NSEventModifierFlagShift) ==
item.keyEquivalentModifierMask;
}
// Returns the NSMenuItem that has the given keyEquivalent and modifiers, or
// nil.
NSMenuItem* MenuContainsAccelerator(NSMenu* menu,
NSString* key_equivalent,
NSUInteger modifier_mask) {
for (NSMenuItem* item in menu.itemArray) {
NSMenu* submenu = item.submenu;
if (submenu) {
NSMenuItem* result =
MenuContainsAccelerator(submenu, key_equivalent, modifier_mask);
if (result)
return result;
}
if ([item.keyEquivalent isEqual:key_equivalent]) {
// We don't want to ignore shift for [cmd + shift + tab] and [cmd + tab],
// which are special.
if (item.tag == IDC_SELECT_NEXT_TAB ||
item.tag == IDC_SELECT_PREVIOUS_TAB) {
if (modifier_mask == item.keyEquivalentModifierMask)
return item;
continue;
}
if (MenuItemHasModifierMask(item, modifier_mask))
return item;
}
}
return nil;
}
} // namespace
class AcceleratorsCocoaBrowserTestRTL : public AcceleratorsCocoaBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
command_line->AppendSwitchASCII(::switches::kForceUIDirection,
::switches::kForceDirectionRTL);
command_line->AppendSwitchASCII(::switches::kForceTextDirection,
::switches::kForceDirectionRTL);
}
};
// Checks that each NSMenuItem in the main menu has a corresponding accelerator,
// and the keyEquivalent/modifiers match.
IN_PROC_BROWSER_TEST_F(AcceleratorsCocoaBrowserTest,
MainMenuAcceleratorsInMapping) {
NSMenu* menu = [NSApp mainMenu];
NSMutableArray* array = [NSMutableArray array];
AddAcceleratorItemsToArray(menu, array);
AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
for (NSMenuItem* item in array) {
const ui::Accelerator* accelerator =
keymap->GetAcceleratorForCommand(item.tag);
EXPECT_TRUE(accelerator);
if (!accelerator) {
continue;
}
// Get the Cocoa key_equivalent associated with the accelerator.
KeyEquivalentAndModifierMask* equivalent =
GetKeyEquivalentAndModifierMaskFromAccelerator(*accelerator);
// Check that the menu item's keyEquivalent matches the one from the
// Cocoa accelerator map.
EXPECT_NSEQ(equivalent.keyEquivalent, item.keyEquivalent);
// Check that the menu item's modifier mask matches the one stored in the
// accelerator. Ignore the NSEventModifierFlagShift because it's part of
// the key equivalent (i.e. "a" + NSEventModifierFlagShift = "A").
EXPECT_TRUE(MenuItemHasModifierMask(item, equivalent.modifierMask));
}
}
// Check that each accelerator with a command_id has an associated NSMenuItem
// in the main menu. If the selector is commandDispatch:, then the tag must
// match the command_id.
IN_PROC_BROWSER_TEST_F(AcceleratorsCocoaBrowserTest,
MappingAcceleratorsInMainMenu) {
AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
// The "Share" menu is dynamically populated.
NSMenu* mainMenu = NSApp.mainMenu;
NSMenu* fileMenu = [[mainMenu itemWithTag:IDC_FILE_MENU] submenu];
NSMenu* shareMenu =
[[fileMenu itemWithTitle:l10n_util::GetNSString(IDS_SHARE_MAC)] submenu];
[[shareMenu delegate] menuNeedsUpdate:shareMenu];
for (auto& it : keymap->accelerators_) {
KeyEquivalentAndModifierMask* equivalent =
GetKeyEquivalentAndModifierMaskFromAccelerator(it.second);
// Check that there exists a corresponding NSMenuItem.
NSMenuItem* item = MenuContainsAccelerator(
[NSApp mainMenu], equivalent.keyEquivalent, equivalent.modifierMask);
EXPECT_TRUE(item);
// If the menu uses a commandDispatch:, the tag must match the command id!
// Added an exception for IDC_TOGGLE_FULLSCREEN_TOOLBAR, which conflicts
// with IDC_PRESENTATION_MODE.
if (item.action == @selector(commandDispatch:) &&
item.tag != IDC_TOGGLE_FULLSCREEN_TOOLBAR) {
EXPECT_EQ(item.tag, it.first);
}
}
}
IN_PROC_BROWSER_TEST_F(AcceleratorsCocoaBrowserTestRTL,
HistoryAcceleratorsReversedForRTL) {
AcceleratorsCocoa* keymap = AcceleratorsCocoa::GetInstance();
ui::Accelerator history_forward = keymap->accelerators_[IDC_FORWARD];
ui::Accelerator history_back = keymap->accelerators_[IDC_BACK];
// In LTR, History -> Forward is VKEY_OEM_6 and Back is VKEY_OEM_4.
EXPECT_EQ(ui::VKEY_OEM_4, history_forward.key_code());
EXPECT_EQ(ui::VKEY_OEM_6, history_back.key_code());
}