chromium/content/browser/cocoa/system_hotkey_map.mm

// 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