// 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 "content/browser/cocoa/system_hotkey_map.h"
#import <Carbon/Carbon.h>
#include "base/apple/foundation_util.h"
#pragma mark - SystemHotkey
namespace content {
struct SystemHotkey {
unsigned short key_code;
NSUInteger modifiers;
};
#pragma mark - SystemHotkeyMap
SystemHotkeyMap::SystemHotkeyMap() = default;
SystemHotkeyMap::SystemHotkeyMap(SystemHotkeyMap&&) = default;
SystemHotkeyMap::~SystemHotkeyMap() = default;
bool SystemHotkeyMap::ParseDictionary(NSDictionary* dictionary) {
system_hotkeys_.clear();
if (!dictionary) {
return false;
}
NSDictionary* user_hotkey_dictionaries =
base::apple::ObjCCast<NSDictionary>(dictionary[@"AppleSymbolicHotKeys"]);
if (!user_hotkey_dictionaries) {
return false;
}
// Start with a dictionary of default macOS hotkeys that are not necessarily
// listed in com.apple.symbolichotkeys.plist, but should still be handled as
// reserved.
// If the user has overridden or disabled any of these hotkeys,
// [NSMutableDictionary addEntriesFromDictionary:] will ensure that the new
// values are used.
// See https://crbug.com/145062#c8
NSMutableDictionary* hotkey_dictionaries = [@{
// Default Window switch key binding: Command + `
// Note: The first parameter @96 is not used by |SystemHotkeyMap|.
@"27" : @{
@"enabled" : @YES,
@"value" : @{
@"type" : @"standard",
@"parameters" : @[
@96 /* unused */, @(kVK_ANSI_Grave), @(NSEventModifierFlagCommand)
],
}
}
} mutableCopy];
[hotkey_dictionaries addEntriesFromDictionary:user_hotkey_dictionaries];
// The meanings of the keys in `user_hotkey_dictionaries` are listed here:
// https://web.archive.org/web/20141112224103/http://hintsforums.macworld.com/showthread.php?t=114785
// Of particular interest are the following:
//
// # Spaces Left - Control, Left
// 79 = { enabled = 1; ... };
//
// # Spaces Right - Control, Right
// 81 = { enabled = 1; ... };
//
// Apparently, when you change the shortcuts for Spaces Left/Right, macOS
// also inserts entries at slots 80 and 82 which differ from the previous
// entries by the addition of the Shift key. This is similar to entries 60
// and 61 as documented in the web page above where Command, Option, Space
// cycles to the previous input source and Command, Option, Space, Shift
// cycles to the next. This approach doesn't make sense for moving between
// Spaces using the arrow keys. Maybe there's legacy machinery in the AppKit
// that automatically creates the shifted versions and macOS knows to ignore
// them (not expecting any non-system applications to read this file).
//
// Treating these shortcuts as valid results in unexpected behavior. For
// example, in the case of "Spaces Left" being mapped to Option Left Arrow,
// Chrome would silently ignore Shift-Option Left Arrow, the shortcut which
// appends the current text selection by one word to the left. To avoid
// this, we'll ignore these two undocumented shortcuts.
// See https://crbug.com/874219 .
const NSString* kSpacesLeftShiftedHotkeyId = @"80";
const NSString* kSpacesRightShiftedHotkeyId = @"82";
[hotkey_dictionaries removeObjectForKey:kSpacesLeftShiftedHotkeyId];
[hotkey_dictionaries removeObjectForKey:kSpacesRightShiftedHotkeyId];
for (NSString* system_hotkey_identifier in [hotkey_dictionaries allKeys]) {
if (![system_hotkey_identifier isKindOfClass:[NSString class]]) {
continue;
}
NSDictionary* hotkey_dictionary = base::apple::ObjCCast<NSDictionary>(
hotkey_dictionaries[system_hotkey_identifier]);
if (!hotkey_dictionary) {
continue;
}
NSNumber* enabled =
base::apple::ObjCCast<NSNumber>(hotkey_dictionary[@"enabled"]);
if (!enabled.boolValue) {
continue;
}
NSDictionary* value =
base::apple::ObjCCast<NSDictionary>(hotkey_dictionary[@"value"]);
if (!value) {
continue;
}
NSString* type = base::apple::ObjCCast<NSString>(value[@"type"]);
if (![type isEqualToString:@"standard"]) {
continue;
}
NSArray* parameters = base::apple::ObjCCast<NSArray>(value[@"parameters"]);
if (parameters.count != 3) {
continue;
}
const int kKeyCodeIndex = 1;
NSNumber* key_code =
base::apple::ObjCCast<NSNumber>(parameters[kKeyCodeIndex]);
if (!key_code) {
continue;
}
const int kModifierIndex = 2;
NSNumber* modifiers =
base::apple::ObjCCast<NSNumber>(parameters[kModifierIndex]);
if (!modifiers) {
continue;
}
ReserveHotkey(key_code.unsignedShortValue, modifiers.unsignedIntegerValue,
system_hotkey_identifier);
}
return true;
}
bool SystemHotkeyMap::IsEventReserved(NSEvent* event) const {
return IsHotkeyReserved(event.keyCode, event.modifierFlags);
}
bool SystemHotkeyMap::IsHotkeyReserved(unsigned short key_code,
NSUInteger modifiers) const {
modifiers &= NSEventModifierFlagDeviceIndependentFlagsMask;
std::vector<SystemHotkey>::const_iterator it;
for (it = system_hotkeys_.begin(); it != system_hotkeys_.end(); ++it) {
if (it->key_code == key_code && it->modifiers == modifiers) {
return true;
}
}
return false;
}
void SystemHotkeyMap::ReserveHotkey(unsigned short key_code,
NSUInteger modifiers,
NSString* system_hotkey_identifier) {
ReserveHotkey(key_code, modifiers);
// If a hotkey exists for cycling through the windows of an application, then
// adding shift to that hotkey cycles through the windows backwards.
NSString* kCycleThroughWindowsHotkeyId = @"27";
const NSUInteger kCycleBackwardsModifier =
modifiers | NSEventModifierFlagShift;
if ([system_hotkey_identifier isEqualToString:kCycleThroughWindowsHotkeyId] &&
modifiers != kCycleBackwardsModifier) {
ReserveHotkey(key_code, kCycleBackwardsModifier);
}
}
void SystemHotkeyMap::ReserveHotkey(unsigned short key_code,
NSUInteger modifiers) {
// Hotkeys require at least one of control, command, or alternate keys to be
// down.
NSUInteger required_modifiers = NSEventModifierFlagControl |
NSEventModifierFlagCommand |
NSEventModifierFlagOption;
if ((modifiers & required_modifiers) == 0) {
return;
}
SystemHotkey hotkey;
hotkey.key_code = key_code;
hotkey.modifiers = modifiers;
system_hotkeys_.push_back(hotkey);
}
} // namespace content