1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195

content / browser / cocoa / system_hotkey_map.mm [blame]

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