summaryrefslogtreecommitdiffstats
path: root/content
diff options
context:
space:
mode:
authorerikchen@chromium.org <erikchen@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-07-31 11:35:51 +0000
committererikchen@chromium.org <erikchen@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2014-07-31 11:35:51 +0000
commitcc405e47896d2b031f8f42a93337538e162ab1e4 (patch)
tree3a77174e1cb82ded7d8b10887a9ed5ac99a01a3f /content
parent97b9212540414b501335f3a0adc7284db1eb0050 (diff)
downloadchromium_src-cc405e47896d2b031f8f42a93337538e162ab1e4.zip
chromium_src-cc405e47896d2b031f8f42a93337538e162ab1e4.tar.gz
chromium_src-cc405e47896d2b031f8f42a93337538e162ab1e4.tar.bz2
mac: Load the system hotkeys after launch. (reland)
---------------Reland CL Description--------------------------- System mouse hotkeys were incorrectly being parsed as system keyboard hotkeys. Other minor changes include: - Expanded unit tests to include a sparsely populated symbolichotkeys.plist from a real machine. - Use correct types for the key_code and modifiers of the event. - An event must have at least one of ctr/alt/cmd down to be considered a hotkey. - Use the NSDeviceIndependentModifierFlagsMask mask to prune out device-dependent modifier flags, including event coalescing information. ---------------Original CL Description--------------------------- Original CL: https://codereview.chromium.org/370293004/ Shortly after launch, the system hotkeys are loaded and parsed. If a hotkey is reserved by the system, it is not passed to the renderer. This allows system hotkeys like (cmd + `) to work even if a flash plugin is selected. Add a histogram to ensure that the system hotkey plist is being correctly loaded and parsed. BUG=383558, 395187 TEST=Open 2 Chrome windows. Navigate one to www.twitch.tv. The site should include a flash plugin that automatically starts playing a video. Select the flash plugin. The hotkey combination (cmd + `) should switch between the open windows. The hotkey combination (cmd + L) should have no effect (it is a Chrome hotkey, not a browser hotkey). Review URL: https://codereview.chromium.org/408973002 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@286737 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'content')
-rw-r--r--content/browser/browser_main_loop.cc2
-rw-r--r--content/browser/cocoa/system_hotkey_helper_mac.h56
-rw-r--r--content/browser/cocoa/system_hotkey_helper_mac.mm77
-rw-r--r--content/browser/cocoa/system_hotkey_map.h66
-rw-r--r--content/browser/cocoa/system_hotkey_map.mm165
-rw-r--r--content/browser/cocoa/system_hotkey_map_unittest.mm196
-rw-r--r--content/browser/renderer_host/render_widget_host_view_mac.mm30
-rw-r--r--content/content_browser.gypi4
-rw-r--r--content/content_tests.gypi1
-rw-r--r--content/test/data/mac/mac_system_hotkeys.plist866
-rw-r--r--content/test/data/mac/mac_system_hotkeys_sparse.plist133
11 files changed, 1596 insertions, 0 deletions
diff --git a/content/browser/browser_main_loop.cc b/content/browser/browser_main_loop.cc
index 6b5cdd4..ce7e224 100644
--- a/content/browser/browser_main_loop.cc
+++ b/content/browser/browser_main_loop.cc
@@ -86,6 +86,7 @@
#if defined(OS_MACOSX) && !defined(OS_IOS)
#include "content/browser/bootstrap_sandbox_mac.h"
+#include "content/browser/cocoa/system_hotkey_helper_mac.h"
#include "content/browser/theme_helper_mac.h"
#endif
@@ -1048,6 +1049,7 @@ int BrowserMainLoop::BrowserThreadsStarted() {
#if defined(OS_MACOSX)
ThemeHelperMac::GetInstance();
+ SystemHotkeyHelperMac::GetInstance()->DeferredLoadSystemHotkeys();
if (ShouldEnableBootstrapSandbox()) {
TRACE_EVENT0("startup",
"BrowserMainLoop::BrowserThreadsStarted:BootstrapSandbox");
diff --git a/content/browser/cocoa/system_hotkey_helper_mac.h b/content/browser/cocoa/system_hotkey_helper_mac.h
new file mode 100644
index 0000000..63d9e51
--- /dev/null
+++ b/content/browser/cocoa/system_hotkey_helper_mac.h
@@ -0,0 +1,56 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_COCOA_SYSTEM_HOTKEY_HELPER_MAC_H_
+#define CONTENT_BROWSER_COCOA_SYSTEM_HOTKEY_HELPER_MAC_H_
+
+#include "base/memory/singleton.h"
+#include "base/memory/weak_ptr.h"
+
+#ifdef __OBJC__
+@class NSDictionary;
+#else
+class NSDictionary;
+#endif
+
+namespace content {
+
+class SystemHotkeyMap;
+
+// This singleton holds a global mapping of hotkeys reserved by OSX.
+class SystemHotkeyHelperMac {
+ public:
+ // Return pointer to the singleton instance for the current process.
+ static SystemHotkeyHelperMac* GetInstance();
+
+ // Loads the system hot keys after a brief delay, to reduce file system access
+ // immediately after launch.
+ void DeferredLoadSystemHotkeys();
+
+ // Guaranteed to not be NULL.
+ SystemHotkeyMap* map() { return map_.get(); }
+
+ private:
+ friend struct DefaultSingletonTraits<SystemHotkeyHelperMac>;
+
+ SystemHotkeyHelperMac();
+ ~SystemHotkeyHelperMac();
+
+ // Must be called from the FILE thread. Loads the file containing the system
+ // hotkeys into a NSDictionary* object, and passes the result to FileDidLoad
+ // on the UI thread.
+ void LoadSystemHotkeys();
+
+ // Must be called from the UI thread. This takes ownership of |dictionary|.
+ // Parses the system hotkeys from the plist stored in |dictionary|.
+ void FileDidLoad(NSDictionary* dictionary);
+
+ scoped_ptr<SystemHotkeyMap> map_;
+
+ DISALLOW_COPY_AND_ASSIGN(SystemHotkeyHelperMac);
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_COCOA_SYSTEM_HOTKEY_HELPER_MAC_H_
diff --git a/content/browser/cocoa/system_hotkey_helper_mac.mm b/content/browser/cocoa/system_hotkey_helper_mac.mm
new file mode 100644
index 0000000..68c5282
--- /dev/null
+++ b/content/browser/cocoa/system_hotkey_helper_mac.mm
@@ -0,0 +1,77 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "content/browser/cocoa/system_hotkey_helper_mac.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/mac/foundation_util.h"
+#include "base/message_loop/message_loop.h"
+#include "base/metrics/histogram.h"
+#include "content/browser/cocoa/system_hotkey_map.h"
+#include "content/public/browser/browser_thread.h"
+
+namespace {
+
+NSString* kSystemHotkeyPlistExtension =
+ @"/Preferences/com.apple.symbolichotkeys.plist";
+
+// Amount of time to delay loading the hotkeys in seconds.
+const int kLoadHotkeysDelaySeconds = 10;
+
+} // namespace
+
+namespace content {
+
+// static
+SystemHotkeyHelperMac* SystemHotkeyHelperMac::GetInstance() {
+ return Singleton<SystemHotkeyHelperMac>::get();
+}
+
+void SystemHotkeyHelperMac::DeferredLoadSystemHotkeys() {
+ BrowserThread::PostDelayedTask(
+ BrowserThread::FILE,
+ FROM_HERE,
+ base::Bind(&SystemHotkeyHelperMac::LoadSystemHotkeys,
+ base::Unretained(this)),
+ base::TimeDelta::FromSeconds(kLoadHotkeysDelaySeconds));
+}
+
+SystemHotkeyHelperMac::SystemHotkeyHelperMac() : map_(new SystemHotkeyMap) {
+}
+
+SystemHotkeyHelperMac::~SystemHotkeyHelperMac() {
+}
+
+void SystemHotkeyHelperMac::LoadSystemHotkeys() {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::FILE));
+
+ std::string library_path(base::mac::GetUserLibraryPath().value());
+ NSString* expanded_file_path =
+ [NSString stringWithFormat:@"%s%@",
+ library_path.c_str(),
+ kSystemHotkeyPlistExtension];
+
+ // Loads the file into memory.
+ NSData* data = [NSData dataWithContentsOfFile:expanded_file_path];
+ // Intentionally create the object with +1 retain count, as FileDidLoad
+ // will destroy the object.
+ NSDictionary* dictionary = [SystemHotkeyMap::DictionaryFromData(data) retain];
+
+ BrowserThread::PostTask(BrowserThread::UI,
+ FROM_HERE,
+ base::Bind(&SystemHotkeyHelperMac::FileDidLoad,
+ base::Unretained(this),
+ dictionary));
+}
+
+void SystemHotkeyHelperMac::FileDidLoad(NSDictionary* dictionary) {
+ DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
+
+ bool success = map()->ParseDictionary(dictionary);
+ UMA_HISTOGRAM_BOOLEAN("OSX.SystemHotkeyMap.LoadSuccess", success);
+ [dictionary release];
+}
+
+} // namespace content
diff --git a/content/browser/cocoa/system_hotkey_map.h b/content/browser/cocoa/system_hotkey_map.h
new file mode 100644
index 0000000..1909aa5
--- /dev/null
+++ b/content/browser/cocoa/system_hotkey_map.h
@@ -0,0 +1,66 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CONTENT_BROWSER_COCOA_SYSTEM_HOTKEY_MAP_H_
+#define CONTENT_BROWSER_COCOA_SYSTEM_HOTKEY_MAP_H_
+
+#import <Cocoa/Cocoa.h>
+#include <vector>
+
+#include "base/gtest_prod_util.h"
+#include "base/macros.h"
+#include "content/common/content_export.h"
+
+namespace content {
+
+struct SystemHotkey;
+
+// Maintains a listing of all OSX system hotkeys. e.g. (cmd + `) These hotkeys
+// should have higher priority than web content, so NSEvents that correspond to
+// a system hotkey should not be passed to the renderer.
+class CONTENT_EXPORT SystemHotkeyMap {
+ public:
+ SystemHotkeyMap();
+ ~SystemHotkeyMap();
+
+ // Converts the plist stored in |data| into an NSDictionary. Returns nil on
+ // error.
+ static NSDictionary* DictionaryFromData(NSData* data);
+
+ // Parses the property list data commonly stored at
+ // ~/Library/Preferences/com.apple.symbolichotkeys.plist
+ // Returns false on encountering an irrecoverable error.
+ // Can be called multiple times. Only the results from the most recent
+ // invocation are stored.
+ bool ParseDictionary(NSDictionary* dictionary);
+
+ // Whether the event corresponds to a hotkey that has been reserved by the
+ // system.
+ bool IsEventReserved(NSEvent* event) const;
+
+ private:
+ FRIEND_TEST_ALL_PREFIXES(SystemHotkeyMapTest, Parse);
+ FRIEND_TEST_ALL_PREFIXES(SystemHotkeyMapTest, ParseCustomEntries);
+ FRIEND_TEST_ALL_PREFIXES(SystemHotkeyMapTest, ParseMouse);
+
+ // Whether the hotkey has been reserved by the user.
+ bool IsHotkeyReserved(unsigned short key_code, NSUInteger modifiers) const;
+
+ // Create at least one record of a hotkey that is reserved by the user.
+ // Certain system hotkeys automatically reserve multiple key combinations.
+ void ReserveHotkey(unsigned short key_code,
+ NSUInteger modifiers,
+ NSString* system_effect);
+
+ // Create a record of a hotkey that is reserved by the user.
+ void ReserveHotkey(unsigned short key_code, NSUInteger modifiers);
+
+ std::vector<SystemHotkey> system_hotkeys_;
+
+ DISALLOW_COPY_AND_ASSIGN(SystemHotkeyMap);
+};
+
+} // namespace content
+
+#endif // CONTENT_BROWSER_COCOA_SYSTEM_HOTKEY_MAP_H_
diff --git a/content/browser/cocoa/system_hotkey_map.mm b/content/browser/cocoa/system_hotkey_map.mm
new file mode 100644
index 0000000..da04fa8
--- /dev/null
+++ b/content/browser/cocoa/system_hotkey_map.mm
@@ -0,0 +1,165 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// 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"
+
+#pragma mark - NSDictionary Helper Functions
+
+namespace {
+
+// All 4 following functions return nil if the object doesn't exist, or isn't of
+// the right class.
+id ObjectForKey(NSDictionary* dict, NSString* key, Class aClass) {
+ id object = [dict objectForKey:key];
+ if (![object isKindOfClass:aClass])
+ return nil;
+ return object;
+}
+
+NSDictionary* DictionaryForKey(NSDictionary* dict, NSString* key) {
+ return ObjectForKey(dict, key, [NSDictionary class]);
+}
+
+NSArray* ArrayForKey(NSDictionary* dict, NSString* key) {
+ return ObjectForKey(dict, key, [NSArray class]);
+}
+
+NSNumber* NumberForKey(NSDictionary* dict, NSString* key) {
+ return ObjectForKey(dict, key, [NSNumber class]);
+}
+
+NSString* StringForKey(NSDictionary* dict, NSString* key) {
+ return ObjectForKey(dict, key, [NSString class]);
+}
+
+} // namespace
+
+#pragma mark - SystemHotkey
+
+namespace content {
+
+struct SystemHotkey {
+ unsigned short key_code;
+ NSUInteger modifiers;
+};
+
+#pragma mark - SystemHotkeyMap
+
+SystemHotkeyMap::SystemHotkeyMap() {
+}
+SystemHotkeyMap::~SystemHotkeyMap() {
+}
+
+NSDictionary* SystemHotkeyMap::DictionaryFromData(NSData* data) {
+ if (!data)
+ return nil;
+
+ NSError* error = nil;
+ NSPropertyListFormat format;
+ NSDictionary* dictionary =
+ [NSPropertyListSerialization propertyListWithData:data
+ options:0
+ format:&format
+ error:&error];
+
+ if (![dictionary isKindOfClass:[NSDictionary class]])
+ return nil;
+
+ return dictionary;
+}
+
+bool SystemHotkeyMap::ParseDictionary(NSDictionary* dictionary) {
+ system_hotkeys_.clear();
+
+ if (!dictionary)
+ return false;
+
+ NSDictionary* hotkey_dictionaries =
+ DictionaryForKey(dictionary, @"AppleSymbolicHotKeys");
+ if (!hotkey_dictionaries)
+ return false;
+
+ for (NSString* hotkey_system_effect in [hotkey_dictionaries allKeys]) {
+ if (![hotkey_system_effect isKindOfClass:[NSString class]])
+ continue;
+
+ NSDictionary* hotkey_dictionary =
+ [hotkey_dictionaries objectForKey:hotkey_system_effect];
+ if (![hotkey_dictionary isKindOfClass:[NSDictionary class]])
+ continue;
+
+ NSNumber* enabled = NumberForKey(hotkey_dictionary, @"enabled");
+ if (!enabled || enabled.boolValue == NO)
+ continue;
+
+ NSDictionary* value = DictionaryForKey(hotkey_dictionary, @"value");
+ if (!value)
+ continue;
+
+ NSString* type = StringForKey(value, @"type");
+ if (!type || ![type isEqualToString:@"standard"])
+ continue;
+
+ NSArray* parameters = ArrayForKey(value, @"parameters");
+ if (!parameters || [parameters count] != 3)
+ continue;
+
+ NSNumber* key_code = [parameters objectAtIndex:1];
+ if (![key_code isKindOfClass:[NSNumber class]])
+ continue;
+
+ NSNumber* modifiers = [parameters objectAtIndex:2];
+ if (![modifiers isKindOfClass:[NSNumber class]])
+ continue;
+
+ ReserveHotkey(key_code.unsignedShortValue,
+ modifiers.unsignedIntegerValue,
+ hotkey_system_effect);
+ }
+
+ 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 &= NSDeviceIndependentModifierFlagsMask;
+ 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_effect) {
+ ReserveHotkey(key_code, modifiers);
+
+ // If a hotkey exists for toggling through the windows of an application, then
+ // adding shift to that hotkey toggles through the windows backwards.
+ if ([system_effect isEqualToString:@"27"])
+ ReserveHotkey(key_code, modifiers | NSShiftKeyMask);
+}
+
+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 =
+ NSControlKeyMask | NSCommandKeyMask | NSAlternateKeyMask;
+ if ((modifiers & required_modifiers) == 0)
+ return;
+
+ SystemHotkey hotkey;
+ hotkey.key_code = key_code;
+ hotkey.modifiers = modifiers;
+ system_hotkeys_.push_back(hotkey);
+}
+
+} // namespace content
diff --git a/content/browser/cocoa/system_hotkey_map_unittest.mm b/content/browser/cocoa/system_hotkey_map_unittest.mm
new file mode 100644
index 0000000..5f088fb
--- /dev/null
+++ b/content/browser/cocoa/system_hotkey_map_unittest.mm
@@ -0,0 +1,196 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+
+#import <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+
+#include "base/files/file_path.h"
+#include "base/mac/scoped_nsobject.h"
+#include "base/path_service.h"
+#import "content/browser/cocoa/system_hotkey_map.h"
+#include "content/public/common/content_paths.h"
+
+namespace content {
+
+class SystemHotkeyMapTest : public ::testing::Test {
+ protected:
+ SystemHotkeyMapTest() {}
+
+ NSData* DataFromTestFile(const char* file) {
+ base::FilePath test_data_dir;
+ bool result = PathService::Get(DIR_TEST_DATA, &test_data_dir);
+ if (!result)
+ return nil;
+
+ base::FilePath test_path = test_data_dir.AppendASCII(file);
+ std::string test_path_string = test_path.AsUTF8Unsafe();
+ NSString* file_path =
+ [NSString stringWithUTF8String:test_path_string.c_str()];
+ return [NSData dataWithContentsOfFile:file_path];
+ }
+
+ void AddEntryToDictionary(BOOL enabled,
+ unsigned short key_code,
+ NSUInteger modifiers) {
+ NSMutableArray* parameters = [NSMutableArray array];
+ // The first parameter is unused.
+ [parameters addObject:[NSNumber numberWithInt:65535]];
+ [parameters addObject:[NSNumber numberWithUnsignedShort:key_code]];
+ [parameters addObject:[NSNumber numberWithUnsignedInteger:modifiers]];
+
+ NSMutableDictionary* value_dictionary = [NSMutableDictionary dictionary];
+ [value_dictionary setObject:parameters forKey:@"parameters"];
+ [value_dictionary setObject:@"standard" forKey:@"type"];
+
+ NSMutableDictionary* outer_dictionary = [NSMutableDictionary dictionary];
+ [outer_dictionary setObject:value_dictionary forKey:@"value"];
+
+ NSNumber* enabled_number = [NSNumber numberWithBool:enabled];
+ [outer_dictionary setObject:enabled_number forKey:@"enabled"];
+
+ NSString* key = [NSString stringWithFormat:@"%d", count_];
+ [system_hotkey_inner_dictionary_ setObject:outer_dictionary forKey:key];
+ ++count_;
+ }
+
+ virtual void SetUp() OVERRIDE {
+ system_hotkey_dictionary_.reset([[NSMutableDictionary alloc] init]);
+ system_hotkey_inner_dictionary_.reset([[NSMutableDictionary alloc] init]);
+ [system_hotkey_dictionary_ setObject:system_hotkey_inner_dictionary_
+ forKey:@"AppleSymbolicHotKeys"];
+ count_ = 100;
+ }
+
+ virtual void TearDown() OVERRIDE {
+ system_hotkey_dictionary_.reset();
+ system_hotkey_inner_dictionary_.reset();
+ }
+
+ // A constructed dictionary that matches the format of the one that would be
+ // parsed from the system hotkeys plist.
+ base::scoped_nsobject<NSMutableDictionary> system_hotkey_dictionary_;
+
+ private:
+ // A reference to the mutable dictionary to which new entries are added.
+ base::scoped_nsobject<NSMutableDictionary> system_hotkey_inner_dictionary_;
+ // Each entry in the system_hotkey_inner_dictionary_ needs to have a unique
+ // key. This count is used to generate those unique keys.
+ int count_;
+};
+
+TEST_F(SystemHotkeyMapTest, Parse) {
+ // This plist was pulled from a real machine. It is extensively populated,
+ // and has no missing or incomplete entries.
+ NSData* data = DataFromTestFile("mac/mac_system_hotkeys.plist");
+ ASSERT_TRUE(data);
+
+ NSDictionary* dictionary = SystemHotkeyMap::DictionaryFromData(data);
+ ASSERT_TRUE(dictionary);
+
+ SystemHotkeyMap map;
+ bool result = map.ParseDictionary(dictionary);
+ EXPECT_TRUE(result);
+
+ // Command + ` is a common key binding. It should exist.
+ unsigned short key_code = kVK_ANSI_Grave;
+ NSUInteger modifiers = NSCommandKeyMask;
+ EXPECT_TRUE(map.IsHotkeyReserved(key_code, modifiers));
+
+ // Command + Shift + ` is a common key binding. It should exist.
+ modifiers = NSCommandKeyMask | NSShiftKeyMask;
+ EXPECT_TRUE(map.IsHotkeyReserved(key_code, modifiers));
+
+ // Command + Shift + Ctr + ` is not a common key binding.
+ modifiers = NSCommandKeyMask | NSShiftKeyMask | NSControlKeyMask;
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, modifiers));
+
+ // Command + L is not a common key binding.
+ key_code = kVK_ANSI_L;
+ modifiers = NSCommandKeyMask;
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, modifiers));
+}
+
+TEST_F(SystemHotkeyMapTest, ParseNil) {
+ NSDictionary* dictionary = nil;
+
+ SystemHotkeyMap map;
+ bool result = map.ParseDictionary(dictionary);
+ EXPECT_FALSE(result);
+}
+
+TEST_F(SystemHotkeyMapTest, ParseMouse) {
+ // This plist was pulled from a real machine. It has missing entries,
+ // incomplete entries, and mouse hotkeys.
+ NSData* data = DataFromTestFile("mac/mac_system_hotkeys_sparse.plist");
+ ASSERT_TRUE(data);
+
+ NSDictionary* dictionary = SystemHotkeyMap::DictionaryFromData(data);
+ ASSERT_TRUE(dictionary);
+
+ SystemHotkeyMap map;
+ bool result = map.ParseDictionary(dictionary);
+ EXPECT_TRUE(result);
+
+ // Command + ` is a common key binding. It is missing.
+ // TODO(erikchen): OSX uses the default value when the keybinding is missing,
+ // so the hotkey should still be reserved.
+ // http://crbug.com/383558
+ unsigned short key_code = kVK_ANSI_Grave;
+ NSUInteger modifiers = NSCommandKeyMask;
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, modifiers));
+
+ // There is a mouse keybinding for 0x08. It should not apply to keyboard
+ // hotkeys.
+ key_code = kVK_ANSI_C;
+ modifiers = 0;
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, modifiers));
+
+ // Command + Alt + = is an accessibility shortcut. Its entry in the plist is
+ // incomplete.
+ // TODO(erikchen): OSX uses the default bindings, so this hotkey should still
+ // be reserved.
+ // http://crbug.com/383558
+ key_code = kVK_ANSI_Equal;
+ modifiers = NSCommandKeyMask | NSAlternateKeyMask;
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, modifiers));
+}
+
+TEST_F(SystemHotkeyMapTest, ParseCustomEntries) {
+ unsigned short key_code = kVK_ANSI_C;
+
+ AddEntryToDictionary(YES, key_code, 0);
+ AddEntryToDictionary(YES, key_code, NSAlphaShiftKeyMask);
+ AddEntryToDictionary(YES, key_code, NSShiftKeyMask);
+ AddEntryToDictionary(YES, key_code, NSControlKeyMask);
+ AddEntryToDictionary(YES, key_code, NSFunctionKeyMask);
+ AddEntryToDictionary(YES, key_code, NSFunctionKeyMask | NSControlKeyMask);
+ AddEntryToDictionary(NO, key_code, NSAlternateKeyMask);
+
+ SystemHotkeyMap map;
+
+ bool result = map.ParseDictionary(system_hotkey_dictionary_.get());
+ EXPECT_TRUE(result);
+
+ // Entries without control, command, or alternate key mask should not be
+ // reserved.
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, 0));
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, NSAlphaShiftKeyMask));
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, NSShiftKeyMask));
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, NSFunctionKeyMask));
+
+ // Unlisted entries should not be reserved.
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, NSCommandKeyMask));
+
+ // Disabled entries should not be reserved.
+ EXPECT_FALSE(map.IsHotkeyReserved(key_code, NSAlternateKeyMask));
+
+ // Other entries should be reserved.
+ EXPECT_TRUE(map.IsHotkeyReserved(key_code, NSControlKeyMask));
+ EXPECT_TRUE(
+ map.IsHotkeyReserved(key_code, NSFunctionKeyMask | NSControlKeyMask));
+}
+
+} // namespace content
diff --git a/content/browser/renderer_host/render_widget_host_view_mac.mm b/content/browser/renderer_host/render_widget_host_view_mac.mm
index b26950a..e60324a 100644
--- a/content/browser/renderer_host/render_widget_host_view_mac.mm
+++ b/content/browser/renderer_host/render_widget_host_view_mac.mm
@@ -28,6 +28,8 @@
#include "base/sys_info.h"
#import "content/browser/accessibility/browser_accessibility_cocoa.h"
#include "content/browser/accessibility/browser_accessibility_manager_mac.h"
+#import "content/browser/cocoa/system_hotkey_helper_mac.h"
+#import "content/browser/cocoa/system_hotkey_map.h"
#include "content/browser/compositor/resize_lock.h"
#include "content/browser/frame_host/frame_tree.h"
#include "content/browser/frame_host/frame_tree_node.h"
@@ -96,6 +98,17 @@ using blink::WebMouseEvent;
using blink::WebMouseWheelEvent;
using blink::WebGestureEvent;
+namespace {
+
+// Whether a keyboard event has been reserved by OSX.
+BOOL EventIsReservedBySystem(NSEvent* event) {
+ content::SystemHotkeyHelperMac* helper =
+ content::SystemHotkeyHelperMac::GetInstance();
+ return helper->map()->IsEventReserved(event);
+}
+
+} // namespace
+
// These are not documented, so use only after checking -respondsToSelector:.
@interface NSApplication (UndocumentedSpeechMethods)
- (void)speakString:(NSString*)string;
@@ -2556,6 +2569,10 @@ void RenderWidgetHostViewMac::OnDisplayMetricsChanged(
if ([[self window] firstResponder] != self)
return NO;
+ // If the event is reserved by the system, then do not pass it to web content.
+ if (EventIsReservedBySystem(theEvent))
+ return NO;
+
// If we return |NO| from this function, cocoa will send the key event to
// the menu and only if the menu does not process the event to |keyDown:|. We
// want to send the event to a renderer _before_ sending it to the menu, so
@@ -2603,6 +2620,19 @@ void RenderWidgetHostViewMac::OnDisplayMetricsChanged(
- (void)keyEvent:(NSEvent*)theEvent wasKeyEquivalent:(BOOL)equiv {
TRACE_EVENT0("browser", "RenderWidgetHostViewCocoa::keyEvent");
+
+ // If the user changes the system hotkey mapping after Chrome has been
+ // launched, then it is possible that a formerly reserved system hotkey is no
+ // longer reserved. The hotkey would have skipped the renderer, but would
+ // also have not been handled by the system. If this is the case, immediately
+ // return.
+ // TODO(erikchen): SystemHotkeyHelperMac should use the File System Events
+ // api to monitor changes to system hotkeys. This logic will have to be
+ // updated.
+ // http://crbug.com/383558.
+ if (EventIsReservedBySystem(theEvent))
+ return;
+
DCHECK([theEvent type] != NSKeyDown ||
!equiv == !([theEvent modifierFlags] & NSCommandKeyMask));
diff --git a/content/content_browser.gypi b/content/content_browser.gypi
index 8adb26e..bc7356d 100644
--- a/content/content_browser.gypi
+++ b/content/content_browser.gypi
@@ -412,6 +412,10 @@
'browser/child_process_launcher.h',
'browser/child_process_security_policy_impl.cc',
'browser/child_process_security_policy_impl.h',
+ 'browser/cocoa/system_hotkey_helper_mac.h',
+ 'browser/cocoa/system_hotkey_helper_mac.mm',
+ 'browser/cocoa/system_hotkey_map.h',
+ 'browser/cocoa/system_hotkey_map.mm',
'browser/cross_site_request_manager.cc',
'browser/cross_site_request_manager.h',
'browser/devtools/devtools_agent_host_impl.cc',
diff --git a/content/content_tests.gypi b/content/content_tests.gypi
index 61f541c..5aa4712 100644
--- a/content/content_tests.gypi
+++ b/content/content_tests.gypi
@@ -407,6 +407,7 @@
'browser/browser_url_handler_impl_unittest.cc',
'browser/byte_stream_unittest.cc',
'browser/child_process_security_policy_unittest.cc',
+ 'browser/cocoa/system_hotkey_map_unittest.mm',
'browser/compositor/software_browser_compositor_output_surface_unittest.cc',
'browser/compositor/software_output_device_ozone_unittest.cc',
'browser/databases_table_unittest.cc',
diff --git a/content/test/data/mac/mac_system_hotkeys.plist b/content/test/data/mac/mac_system_hotkeys.plist
new file mode 100644
index 0000000..c541463
--- /dev/null
+++ b/content/test/data/mac/mac_system_hotkeys.plist
@@ -0,0 +1,866 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>AppleSymbolicHotKeys</key>
+ <dict>
+ <key>10</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>96</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>11</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>97</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>118</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>18</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>119</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>19</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>12</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>122</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>120</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>20</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>121</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>21</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>13</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>98</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>15</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>56</integer>
+ <integer>28</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>16</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>160</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>65535</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>162</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>96</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>163</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>65535</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>17</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>61</integer>
+ <integer>24</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>175</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>65535</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>18</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>19</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>45</integer>
+ <integer>27</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>20</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>21</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>56</integer>
+ <integer>28</integer>
+ <integer>1835008</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>22</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>23</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>92</integer>
+ <integer>42</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>24</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>25</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>46</integer>
+ <integer>47</integer>
+ <integer>1835008</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>26</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>44</integer>
+ <integer>43</integer>
+ <integer>1835008</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>27</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>96</integer>
+ <integer>50</integer>
+ <integer>1048576</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>28</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>51</integer>
+ <integer>20</integer>
+ <integer>1179648</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>29</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>51</integer>
+ <integer>20</integer>
+ <integer>1441792</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>30</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>52</integer>
+ <integer>21</integer>
+ <integer>1179648</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>31</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>52</integer>
+ <integer>21</integer>
+ <integer>1441792</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>32</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>126</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>33</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>125</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>34</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>126</integer>
+ <integer>393216</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>35</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>125</integer>
+ <integer>393216</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>36</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>103</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>37</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>103</integer>
+ <integer>131072</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>51</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>39</integer>
+ <integer>50</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>52</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>100</integer>
+ <integer>2</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>53</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>107</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>54</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>113</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>55</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>107</integer>
+ <integer>524288</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>56</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>113</integer>
+ <integer>524288</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>57</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>100</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>59</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>96</integer>
+ <integer>1048576</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>60</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>32</integer>
+ <integer>49</integer>
+ <integer>1048576</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>61</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>32</integer>
+ <integer>49</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>62</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>111</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>63</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>111</integer>
+ <integer>131072</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>64</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>49</integer>
+ <integer>1048576</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>65</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>49</integer>
+ <integer>1572864</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>7</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>120</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>79</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>123</integer>
+ <integer>3932160</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>8</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>99</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>80</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>123</integer>
+ <integer>4063232</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>81</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>124</integer>
+ <integer>3932160</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>82</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>124</integer>
+ <integer>4063232</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>9</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>65535</integer>
+ <integer>118</integer>
+ <integer>262144</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ <key>98</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>47</integer>
+ <integer>44</integer>
+ <integer>1179648</integer>
+ </array>
+ <key>type</key>
+ <string>standard</string>
+ </dict>
+ </dict>
+ </dict>
+</dict>
+</plist>
diff --git a/content/test/data/mac/mac_system_hotkeys_sparse.plist b/content/test/data/mac/mac_system_hotkeys_sparse.plist
new file mode 100644
index 0000000..ffda3af
--- /dev/null
+++ b/content/test/data/mac/mac_system_hotkeys_sparse.plist
@@ -0,0 +1,133 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>AppleSymbolicHotKeys</key>
+ <dict>
+ <key>15</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ </dict>
+ <key>16</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>17</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ </dict>
+ <key>18</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>19</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ </dict>
+ <key>20</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>21</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>22</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>23</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ </dict>
+ <key>24</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>25</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>26</key>
+ <dict>
+ <key>enabled</key>
+ <false/>
+ </dict>
+ <key>38</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>8</integer>
+ <integer>8</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>button</string>
+ </dict>
+ </dict>
+ <key>39</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>16</integer>
+ <integer>16</integer>
+ <integer>0</integer>
+ </array>
+ <key>type</key>
+ <string>button</string>
+ </dict>
+ </dict>
+ <key>40</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>8</integer>
+ <integer>8</integer>
+ <integer>131072</integer>
+ </array>
+ <key>type</key>
+ <string>button</string>
+ </dict>
+ </dict>
+ <key>41</key>
+ <dict>
+ <key>enabled</key>
+ <true/>
+ <key>value</key>
+ <dict>
+ <key>parameters</key>
+ <array>
+ <integer>16</integer>
+ <integer>16</integer>
+ <integer>131072</integer>
+ </array>
+ <key>type</key>
+ <string>button</string>
+ </dict>
+ </dict>
+ </dict>
+</dict>
+</plist>