aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTomáš Znamenáček2015-01-08 12:00:53 +0100
committerTomáš Znamenáček2015-01-08 12:00:53 +0100
commit9b919cba51e4cd11b0c4424930d6c18a1baec73c (patch)
treea8107774609d5f4263f7b79749d93e6c6ff2642d
parenta3a459b4e4e47bf18dccd5dc7f315389346e3d6c (diff)
parentea69d5939511f61a7082ba1e8ff46d247862a3fa (diff)
downloadMASShortcut-9b919cba51e4cd11b0c4424930d6c18a1baec73c.tar.bz2
Merge pull request #53 from zoul/2.0-candidate
Thank you very much!
-rw-r--r--CHANGES4
-rw-r--r--Demo/AppDelegate.h10
-rw-r--r--Demo/AppDelegate.m90
-rw-r--r--Demo/Info.plist34
-rw-r--r--Demo/MainMenu.xib712
-rw-r--r--Demo/Prefix.pch2
-rw-r--r--Demo/main.m4
-rw-r--r--Framework/Info.plist24
-rw-r--r--Framework/MASDictionaryTransformer.h19
-rw-r--r--Framework/MASDictionaryTransformer.m51
-rw-r--r--Framework/MASDictionaryTransformerTests.m32
-rw-r--r--Framework/MASHotKey.h12
-rw-r--r--Framework/MASHotKey.m44
-rw-r--r--Framework/MASKeyCodes.h42
-rw-r--r--Framework/MASShortcut.h70
-rw-r--r--Framework/MASShortcut.m241
-rw-r--r--Framework/MASShortcutBinder.h67
-rw-r--r--Framework/MASShortcutBinder.m114
-rw-r--r--Framework/MASShortcutBinderTests.m98
-rw-r--r--Framework/MASShortcutMonitor.h27
-rw-r--r--Framework/MASShortcutMonitor.m101
-rw-r--r--Framework/MASShortcutTests.m26
-rw-r--r--Framework/MASShortcutValidator.h15
-rw-r--r--Framework/MASShortcutValidator.m111
-rw-r--r--Framework/MASShortcutView+Bindings.h25
-rw-r--r--Framework/MASShortcutView+Bindings.m50
-rw-r--r--Framework/MASShortcutView.h24
-rw-r--r--Framework/MASShortcutView.m (renamed from MASShortcutView.m)130
-rw-r--r--Framework/Prefix.pch2
-rw-r--r--Framework/Shortcut.h7
-rw-r--r--MASShortcut+Monitoring.h8
-rw-r--r--MASShortcut+Monitoring.m165
-rw-r--r--MASShortcut+UserDefaults.h9
-rw-r--r--MASShortcut+UserDefaults.m98
-rw-r--r--MASShortcut.h60
-rw-r--r--MASShortcut.m354
-rw-r--r--MASShortcut.podspec12
-rw-r--r--MASShortcut.xcodeproj/project.pbxproj652
-rw-r--r--MASShortcutView+UserDefaults.h7
-rw-r--r--MASShortcutView+UserDefaults.m125
-rw-r--r--MASShortcutView.h23
-rw-r--r--README.md29
-rw-r--r--Spec.md15
-rw-r--r--Tests/Info.plist22
-rw-r--r--Tests/Prefix.pch5
45 files changed, 2865 insertions, 907 deletions
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..df1e9ad
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,4 @@
+2.0.0 unreleased yet
+ - Major refactoring to simplify long-term maintenance.
+ - First version with a changes file :)
+ - Backwards incompatible.
diff --git a/Demo/AppDelegate.h b/Demo/AppDelegate.h
new file mode 100644
index 0000000..5053cfb
--- /dev/null
+++ b/Demo/AppDelegate.h
@@ -0,0 +1,10 @@
+@class MASShortcutView;
+
+@interface AppDelegate : NSObject <NSApplicationDelegate>
+
+@property (nonatomic, assign) IBOutlet NSWindow *window;
+@property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView;
+@property (nonatomic, getter = isShortcutEnabled) BOOL shortcutEnabled;
+@property (nonatomic, getter = isConstantShortcutEnabled) BOOL constantShortcutEnabled;
+
+@end
diff --git a/Demo/AppDelegate.m b/Demo/AppDelegate.m
new file mode 100644
index 0000000..7aff3d1
--- /dev/null
+++ b/Demo/AppDelegate.m
@@ -0,0 +1,90 @@
+#import "AppDelegate.h"
+
+NSString *const MASPreferenceKeyShortcut = @"MASDemoShortcut";
+NSString *const MASPreferenceKeyShortcutEnabled = @"MASDemoShortcutEnabled";
+NSString *const MASPreferenceKeyConstantShortcutEnabled = @"MASDemoConstantShortcutEnabled";
+
+@implementation AppDelegate
+
+#pragma mark -
+
+- (void)awakeFromNib
+{
+ [super awakeFromNib];
+ // Checkbox will enable and disable the shortcut view
+ [self.shortcutView bind:@"enabled" toObject:self withKeyPath:@"shortcutEnabled" options:nil];
+}
+
+#pragma mark NSApplicationDelegate
+
+- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
+{
+ // Shortcut view will follow and modify user preferences automatically
+ [_shortcutView setAssociatedUserDefaultsKey:MASPreferenceKeyShortcut];
+
+ // Activate the global keyboard shortcut if it was enabled last time
+ [self resetShortcutRegistration];
+
+ // Activate the shortcut Command-F1 if it was enabled
+ [self resetConstantShortcutRegistration];
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
+{
+ return YES;
+}
+
+#pragma mark - Custom shortcut
+
+- (BOOL)isShortcutEnabled
+{
+ return [[NSUserDefaults standardUserDefaults] boolForKey:MASPreferenceKeyShortcutEnabled];
+}
+
+- (void)setShortcutEnabled:(BOOL)enabled
+{
+ if (self.shortcutEnabled != enabled) {
+ [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:MASPreferenceKeyShortcutEnabled];
+ [self resetShortcutRegistration];
+ }
+}
+
+- (void)resetShortcutRegistration
+{
+ if (self.shortcutEnabled) {
+ [[MASShortcutBinder sharedBinder] bindShortcutWithDefaultsKey:MASPreferenceKeyShortcut toAction:^{
+ [[NSSound soundNamed:@"Ping"] play];
+ }];
+ } else {
+ [[MASShortcutBinder sharedBinder] breakBindingWithDefaultsKey:MASPreferenceKeyShortcut];
+ }
+}
+
+#pragma mark - Constant shortcut
+
+- (BOOL)isConstantShortcutEnabled
+{
+ return [[NSUserDefaults standardUserDefaults] boolForKey:MASPreferenceKeyConstantShortcutEnabled];
+}
+
+- (void)setConstantShortcutEnabled:(BOOL)enabled
+{
+ if (self.constantShortcutEnabled != enabled) {
+ [[NSUserDefaults standardUserDefaults] setBool:enabled forKey:MASPreferenceKeyConstantShortcutEnabled];
+ [self resetConstantShortcutRegistration];
+ }
+}
+
+- (void)resetConstantShortcutRegistration
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:kVK_ANSI_Keypad2 modifierFlags:NSCommandKeyMask];
+ if (self.constantShortcutEnabled) {
+ [[MASShortcutMonitor sharedMonitor] registerShortcut:shortcut withAction:^{
+ [[NSSound soundNamed:@"Ping"] play];
+ }];
+ } else {
+ [[MASShortcutMonitor sharedMonitor] unregisterShortcut:shortcut];
+ }
+}
+
+@end
diff --git a/Demo/Info.plist b/Demo/Info.plist
new file mode 100644
index 0000000..083b650
--- /dev/null
+++ b/Demo/Info.plist
@@ -0,0 +1,34 @@
+<?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>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>com.shpakovski.mac.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>${MACOSX_DEPLOYMENT_TARGET}</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2012 Vadim Shpakovski. All rights reserved.</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+</dict>
+</plist>
diff --git a/Demo/MainMenu.xib b/Demo/MainMenu.xib
new file mode 100644
index 0000000..a7d6806
--- /dev/null
+++ b/Demo/MainMenu.xib
@@ -0,0 +1,712 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="5056" systemVersion="13E28" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="5056"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
+ <connections>
+ <outlet property="delegate" destination="494" id="495"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application"/>
+ <menu title="AMainMenu" systemMenu="main" id="29">
+ <items>
+ <menuItem title="Demo" id="56">
+ <menu key="submenu" title="Demo" systemMenu="apple" id="57">
+ <items>
+ <menuItem title="About Demo" id="58">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="orderFrontStandardAboutPanel:" target="-2" id="142"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="236">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Preferences…" keyEquivalent="," id="129"/>
+ <menuItem isSeparatorItem="YES" id="143">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Services" id="131">
+ <menu key="submenu" title="Services" systemMenu="services" id="130"/>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="144">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Hide Demo" keyEquivalent="h" id="134">
+ <connections>
+ <action selector="hide:" target="-1" id="367"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Hide Others" keyEquivalent="h" id="145">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="hideOtherApplications:" target="-1" id="368"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Show All" id="150">
+ <connections>
+ <action selector="unhideAllApplications:" target="-1" id="370"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="149">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Quit Demo" keyEquivalent="q" id="136">
+ <connections>
+ <action selector="terminate:" target="-3" id="449"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="File" id="83">
+ <menu key="submenu" title="File" id="81">
+ <items>
+ <menuItem title="New" keyEquivalent="n" id="82">
+ <connections>
+ <action selector="newDocument:" target="-1" id="373"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Open…" keyEquivalent="o" id="72">
+ <connections>
+ <action selector="openDocument:" target="-1" id="374"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Open Recent" id="124">
+ <menu key="submenu" title="Open Recent" systemMenu="recentDocuments" id="125">
+ <items>
+ <menuItem title="Clear Menu" id="126">
+ <connections>
+ <action selector="clearRecentDocuments:" target="-1" id="127"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="79">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Close" keyEquivalent="w" id="73">
+ <connections>
+ <action selector="performClose:" target="-1" id="193"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Save…" keyEquivalent="s" id="75">
+ <connections>
+ <action selector="saveDocument:" target="-1" id="362"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Revert to Saved" id="112">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="revertDocumentToSaved:" target="-1" id="364"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="74">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Page Setup..." keyEquivalent="P" id="77">
+ <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+ <connections>
+ <action selector="runPageLayout:" target="-1" id="87"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Print…" keyEquivalent="p" id="78">
+ <connections>
+ <action selector="print:" target="-1" id="86"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Edit" id="217">
+ <menu key="submenu" title="Edit" id="205">
+ <items>
+ <menuItem title="Undo" keyEquivalent="z" id="207">
+ <connections>
+ <action selector="undo:" target="-1" id="223"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Redo" keyEquivalent="Z" id="215">
+ <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+ <connections>
+ <action selector="redo:" target="-1" id="231"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="206">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Cut" keyEquivalent="x" id="199">
+ <connections>
+ <action selector="cut:" target="-1" id="228"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Copy" keyEquivalent="c" id="197">
+ <connections>
+ <action selector="copy:" target="-1" id="224"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste" keyEquivalent="v" id="203">
+ <connections>
+ <action selector="paste:" target="-1" id="226"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste and Match Style" keyEquivalent="V" id="485">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="pasteAsPlainText:" target="-1" id="486"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Delete" id="202">
+ <connections>
+ <action selector="delete:" target="-1" id="235"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Select All" keyEquivalent="a" id="198">
+ <connections>
+ <action selector="selectAll:" target="-1" id="232"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="214">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Find" id="218">
+ <menu key="submenu" title="Find" id="220">
+ <items>
+ <menuItem title="Find…" tag="1" keyEquivalent="f" id="209">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="241"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find and Replace…" tag="12" keyEquivalent="f" id="534">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="535"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find Next" tag="2" keyEquivalent="g" id="208">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="487"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Find Previous" tag="3" keyEquivalent="G" id="213">
+ <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="488"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use Selection for Find" tag="7" keyEquivalent="e" id="221">
+ <connections>
+ <action selector="performFindPanelAction:" target="-1" id="489"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Jump to Selection" keyEquivalent="j" id="210">
+ <connections>
+ <action selector="centerSelectionInVisibleArea:" target="-1" id="245"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Spelling and Grammar" id="216">
+ <menu key="submenu" title="Spelling and Grammar" id="200">
+ <items>
+ <menuItem title="Show Spelling and Grammar" keyEquivalent=":" id="204">
+ <connections>
+ <action selector="showGuessPanel:" target="-1" id="230"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Check Document Now" keyEquivalent=";" id="201">
+ <connections>
+ <action selector="checkSpelling:" target="-1" id="225"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="453"/>
+ <menuItem title="Check Spelling While Typing" id="219">
+ <connections>
+ <action selector="toggleContinuousSpellChecking:" target="-1" id="222"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Check Grammar With Spelling" id="346">
+ <connections>
+ <action selector="toggleGrammarChecking:" target="-1" id="347"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Correct Spelling Automatically" id="454">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticSpellingCorrection:" target="-1" id="456"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Substitutions" id="348">
+ <menu key="submenu" title="Substitutions" id="349">
+ <items>
+ <menuItem title="Show Substitutions" id="457">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="orderFrontSubstitutionsPanel:" target="-1" id="458"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="459"/>
+ <menuItem title="Smart Copy/Paste" tag="1" keyEquivalent="f" id="350">
+ <connections>
+ <action selector="toggleSmartInsertDelete:" target="-1" id="355"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Quotes" tag="2" keyEquivalent="g" id="351">
+ <connections>
+ <action selector="toggleAutomaticQuoteSubstitution:" target="-1" id="356"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Dashes" id="460">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticDashSubstitution:" target="-1" id="461"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smart Links" tag="3" keyEquivalent="G" id="354">
+ <modifierMask key="keyEquivalentModifierMask" shift="YES" command="YES"/>
+ <connections>
+ <action selector="toggleAutomaticLinkDetection:" target="-1" id="357"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Text Replacement" id="462">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleAutomaticTextReplacement:" target="-1" id="463"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Transformations" id="450">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Transformations" id="451">
+ <items>
+ <menuItem title="Make Upper Case" id="452">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="uppercaseWord:" target="-1" id="464"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Make Lower Case" id="465">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="lowercaseWord:" target="-1" id="468"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Capitalize" id="466">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="capitalizeWord:" target="-1" id="467"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Speech" id="211">
+ <menu key="submenu" title="Speech" id="212">
+ <items>
+ <menuItem title="Start Speaking" id="196">
+ <connections>
+ <action selector="startSpeaking:" target="-1" id="233"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Stop Speaking" id="195">
+ <connections>
+ <action selector="stopSpeaking:" target="-1" id="227"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Format" id="375">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Format" id="376">
+ <items>
+ <menuItem title="Font" id="377">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Font" systemMenu="font" id="388">
+ <items>
+ <menuItem title="Show Fonts" keyEquivalent="t" id="389">
+ <connections>
+ <action selector="orderFrontFontPanel:" target="420" id="424"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Bold" tag="2" keyEquivalent="b" id="390">
+ <connections>
+ <action selector="addFontTrait:" target="420" id="421"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Italic" tag="1" keyEquivalent="i" id="391">
+ <connections>
+ <action selector="addFontTrait:" target="420" id="422"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Underline" keyEquivalent="u" id="392">
+ <connections>
+ <action selector="underline:" target="-1" id="432"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="393"/>
+ <menuItem title="Bigger" tag="3" keyEquivalent="+" id="394">
+ <connections>
+ <action selector="modifyFont:" target="420" id="425"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Smaller" tag="4" keyEquivalent="-" id="395">
+ <connections>
+ <action selector="modifyFont:" target="420" id="423"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="396"/>
+ <menuItem title="Kern" id="397">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Kern" id="415">
+ <items>
+ <menuItem title="Use Default" id="416">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="useStandardKerning:" target="-1" id="438"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use None" id="417">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="turnOffKerning:" target="-1" id="441"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Tighten" id="418">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="tightenKerning:" target="-1" id="431"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Loosen" id="419">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="loosenKerning:" target="-1" id="435"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Ligatures" id="398">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Ligatures" id="411">
+ <items>
+ <menuItem title="Use Default" id="412">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="useStandardLigatures:" target="-1" id="439"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use None" id="413">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="turnOffLigatures:" target="-1" id="440"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Use All" id="414">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="useAllLigatures:" target="-1" id="434"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Baseline" id="399">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Baseline" id="405">
+ <items>
+ <menuItem title="Use Default" id="406">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="unscript:" target="-1" id="437"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Superscript" id="407">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="superscript:" target="-1" id="430"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Subscript" id="408">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="subscript:" target="-1" id="429"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Raise" id="409">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="raiseBaseline:" target="-1" id="426"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Lower" id="410">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="lowerBaseline:" target="-1" id="427"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="400"/>
+ <menuItem title="Show Colors" keyEquivalent="C" id="401">
+ <connections>
+ <action selector="orderFrontColorPanel:" target="-1" id="433"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="402"/>
+ <menuItem title="Copy Style" keyEquivalent="c" id="403">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="copyFont:" target="-1" id="428"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste Style" keyEquivalent="v" id="404">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="pasteFont:" target="-1" id="436"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Text" id="496">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Text" id="497">
+ <items>
+ <menuItem title="Align Left" keyEquivalent="{" id="498">
+ <connections>
+ <action selector="alignLeft:" target="-1" id="524"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Center" keyEquivalent="|" id="499">
+ <connections>
+ <action selector="alignCenter:" target="-1" id="518"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Justify" id="500">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="alignJustified:" target="-1" id="523"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Align Right" keyEquivalent="}" id="501">
+ <connections>
+ <action selector="alignRight:" target="-1" id="521"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="502"/>
+ <menuItem title="Writing Direction" id="503">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Writing Direction" id="508">
+ <items>
+ <menuItem title="Paragraph" enabled="NO" id="509">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ </menuItem>
+ <menuItem id="510">
+ <string key="title"> Default</string>
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="makeBaseWritingDirectionNatural:" target="-1" id="525"/>
+ </connections>
+ </menuItem>
+ <menuItem id="511">
+ <string key="title"> Left to Right</string>
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="makeBaseWritingDirectionLeftToRight:" target="-1" id="526"/>
+ </connections>
+ </menuItem>
+ <menuItem id="512">
+ <string key="title"> Right to Left</string>
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="makeBaseWritingDirectionRightToLeft:" target="-1" id="527"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="513"/>
+ <menuItem title="Selection" enabled="NO" id="514">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ </menuItem>
+ <menuItem id="515">
+ <string key="title"> Default</string>
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="makeTextWritingDirectionNatural:" target="-1" id="528"/>
+ </connections>
+ </menuItem>
+ <menuItem id="516">
+ <string key="title"> Left to Right</string>
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="makeTextWritingDirectionLeftToRight:" target="-1" id="529"/>
+ </connections>
+ </menuItem>
+ <menuItem id="517">
+ <string key="title"> Right to Left</string>
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="makeTextWritingDirectionRightToLeft:" target="-1" id="530"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="504"/>
+ <menuItem title="Show Ruler" id="505">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <connections>
+ <action selector="toggleRuler:" target="-1" id="520"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Copy Ruler" keyEquivalent="c" id="506">
+ <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
+ <connections>
+ <action selector="copyRuler:" target="-1" id="522"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Paste Ruler" keyEquivalent="v" id="507">
+ <modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
+ <connections>
+ <action selector="pasteRuler:" target="-1" id="519"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="View" id="295">
+ <menu key="submenu" title="View" id="296">
+ <items>
+ <menuItem title="Show Toolbar" keyEquivalent="t" id="297">
+ <modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
+ <connections>
+ <action selector="toggleToolbarShown:" target="-1" id="366"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Customize Toolbar…" id="298">
+ <connections>
+ <action selector="runToolbarCustomizationPalette:" target="-1" id="365"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Window" id="19">
+ <menu key="submenu" title="Window" systemMenu="window" id="24">
+ <items>
+ <menuItem title="Minimize" keyEquivalent="m" id="23">
+ <connections>
+ <action selector="performMiniaturize:" target="-1" id="37"/>
+ </connections>
+ </menuItem>
+ <menuItem title="Zoom" id="239">
+ <connections>
+ <action selector="performZoom:" target="-1" id="240"/>
+ </connections>
+ </menuItem>
+ <menuItem isSeparatorItem="YES" id="92">
+ <modifierMask key="keyEquivalentModifierMask" command="YES"/>
+ </menuItem>
+ <menuItem title="Bring All to Front" id="5">
+ <connections>
+ <action selector="arrangeInFront:" target="-1" id="39"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ <menuItem title="Help" id="490">
+ <modifierMask key="keyEquivalentModifierMask"/>
+ <menu key="submenu" title="Help" systemMenu="help" id="491">
+ <items>
+ <menuItem title="Demo Help" keyEquivalent="?" id="492">
+ <connections>
+ <action selector="showHelp:" target="-1" id="493"/>
+ </connections>
+ </menuItem>
+ </items>
+ </menu>
+ </menuItem>
+ </items>
+ </menu>
+ <window title="Demo" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" frameAutosaveName="DemoWindow" animationBehavior="default" id="371">
+ <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES"/>
+ <rect key="contentRect" x="335" y="390" width="410" height="132"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="1920" height="1058"/>
+ <view key="contentView" id="372">
+ <rect key="frame" x="0.0" y="0.0" width="410" height="132"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <button horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="639">
+ <rect key="frame" x="131" y="28" width="201" height="18"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <buttonCell key="cell" type="check" title="Also use ⌘2 to play a sound" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="640">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <binding destination="494" name="value" keyPath="constantShortcutEnabled" id="711"/>
+ </connections>
+ </button>
+ <customView fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="536" customClass="MASShortcutView">
+ <rect key="frame" x="133" y="87" width="127" height="19"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ </customView>
+ <button horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="579">
+ <rect key="frame" x="131" y="48" width="261" height="18"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <buttonCell key="cell" type="check" title="Global shortcut to play a sound" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="580">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <binding destination="494" name="value" keyPath="shortcutEnabled" id="638"/>
+ </connections>
+ </button>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="PG0-jh-Onh">
+ <rect key="frame" x="20" y="89" width="105" height="17"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Global shortcut:" id="85u-1A-n7H">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ <textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Ezj-wg-E35">
+ <rect key="frame" x="20" y="50" width="105" height="17"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
+ <textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Options:" id="Wct-tg-3PU">
+ <font key="font" metaFont="system"/>
+ <color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
+ <color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
+ </textFieldCell>
+ </textField>
+ </subviews>
+ </view>
+ </window>
+ <customObject id="494" customClass="AppDelegate">
+ <connections>
+ <outlet property="shortcutView" destination="536" id="548"/>
+ <outlet property="window" destination="371" id="532"/>
+ </connections>
+ </customObject>
+ <customObject id="420" customClass="NSFontManager"/>
+ </objects>
+</document>
diff --git a/Demo/Prefix.pch b/Demo/Prefix.pch
new file mode 100644
index 0000000..20d47b6
--- /dev/null
+++ b/Demo/Prefix.pch
@@ -0,0 +1,2 @@
+#import <Cocoa/Cocoa.h>
+#import <MASShortcut/Shortcut.h>
diff --git a/Demo/main.m b/Demo/main.m
new file mode 100644
index 0000000..c648579
--- /dev/null
+++ b/Demo/main.m
@@ -0,0 +1,4 @@
+int main(int argc, char *argv[])
+{
+ return NSApplicationMain(argc, (const char **)argv);
+}
diff --git a/Framework/Info.plist b/Framework/Info.plist
new file mode 100644
index 0000000..91a62a8
--- /dev/null
+++ b/Framework/Info.plist
@@ -0,0 +1,24 @@
+<?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>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>${PRODUCT_NAME}</string>
+ <key>CFBundlePackageType</key>
+ <string>FMWK</string>
+ <key>CFBundleShortVersionString</key>
+ <string>2.0.0</string>
+ <key>CFBundleVersion</key>
+ <string>2.0.0</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2014–2015 Vadim Shpakovski. All rights reserved.</string>
+</dict>
+</plist>
diff --git a/Framework/MASDictionaryTransformer.h b/Framework/MASDictionaryTransformer.h
new file mode 100644
index 0000000..eced1bb
--- /dev/null
+++ b/Framework/MASDictionaryTransformer.h
@@ -0,0 +1,19 @@
+extern NSString *const MASDictionaryTransformerName;
+
+/**
+ @brief Converts shortcuts for storage in user defaults.
+
+ User defaults can’t stored custom types directly, they have to
+ be serialized to @p NSData or some other supported type like an
+ @p NSDictionary. In Cocoa Bindings, the conversion can be done
+ using value transformers like this one.
+
+ There’s a built-in transformer (@p NSKeyedUnarchiveFromDataTransformerName)
+ that converts any @p NSCoding types to @p NSData, but with shortcuts
+ it makes sense to use a dictionary instead – the defaults look better
+ when inspected with the @p defaults command-line utility and the
+ format is compatible with an older sortcut library called Shortcut
+ Recorder.
+*/
+@interface MASDictionaryTransformer : NSValueTransformer
+@end
diff --git a/Framework/MASDictionaryTransformer.m b/Framework/MASDictionaryTransformer.m
new file mode 100644
index 0000000..9e4c82b
--- /dev/null
+++ b/Framework/MASDictionaryTransformer.m
@@ -0,0 +1,51 @@
+#import "MASDictionaryTransformer.h"
+#import "MASShortcut.h"
+
+NSString *const MASDictionaryTransformerName = @"MASDictionaryTransformer";
+
+static NSString *const MASKeyCodeKey = @"keyCode";
+static NSString *const MASModifierFlagsKey = @"modifierFlags";
+
+@implementation MASDictionaryTransformer
+
++ (BOOL) allowsReverseTransformation
+{
+ return YES;
+}
+
+// Storing nil values as an empty dictionary lets us differ between
+// “not available, use default value” and “explicitly set to none”.
+// See http://stackoverflow.com/questions/5540760 for details.
+- (NSDictionary*) reverseTransformedValue: (MASShortcut*) shortcut
+{
+ if (shortcut == nil) {
+ return [NSDictionary dictionary];
+ } else {
+ return @{
+ MASKeyCodeKey: @([shortcut keyCode]),
+ MASModifierFlagsKey: @([shortcut modifierFlags])
+ };
+ }
+}
+
+- (MASShortcut*) transformedValue: (NSDictionary*) dictionary
+{
+ // We have to be defensive here as the value may come from user defaults.
+ if (![dictionary isKindOfClass:[NSDictionary class]]) {
+ return nil;
+ }
+
+ id keyCodeBox = [dictionary objectForKey:MASKeyCodeKey];
+ id modifierFlagsBox = [dictionary objectForKey:MASModifierFlagsKey];
+
+ SEL integerValue = @selector(integerValue);
+ if (![keyCodeBox respondsToSelector:integerValue] || ![modifierFlagsBox respondsToSelector:integerValue]) {
+ return nil;
+ }
+
+ return [MASShortcut
+ shortcutWithKeyCode:[keyCodeBox integerValue]
+ modifierFlags:[modifierFlagsBox integerValue]];
+}
+
+@end
diff --git a/Framework/MASDictionaryTransformerTests.m b/Framework/MASDictionaryTransformerTests.m
new file mode 100644
index 0000000..48e11f3
--- /dev/null
+++ b/Framework/MASDictionaryTransformerTests.m
@@ -0,0 +1,32 @@
+@interface MASDictionaryTransformerTests : XCTestCase
+@end
+
+@implementation MASDictionaryTransformerTests
+
+- (void) testErrorHandling
+{
+ MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
+ XCTAssertNil([transformer transformedValue:nil],
+ @"Decoding a shortcut from a nil dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:(id)@"foo"],
+ @"Decoding a shortcut from a invalid-type dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{}],
+ @"Decoding a shortcut from an empty dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{@"keyCode":@"foo"}],
+ @"Decoding a shortcut from a wrong-typed dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{@"keyCode":@1}],
+ @"Decoding a shortcut from an incomplete dictionary returns nil.");
+ XCTAssertNil([transformer transformedValue:@{@"modifierFlags":@1}],
+ @"Decoding a shortcut from an incomplete dictionary returns nil.");
+}
+
+- (void) testNilRepresentation
+{
+ MASDictionaryTransformer *transformer = [MASDictionaryTransformer new];
+ XCTAssertEqualObjects([transformer reverseTransformedValue:nil], [NSDictionary dictionary],
+ @"Store nil values as an empty dictionary.");
+ XCTAssertNil([transformer transformedValue:[NSDictionary dictionary]],
+ @"Load empty dictionary as nil.");
+}
+
+@end
diff --git a/Framework/MASHotKey.h b/Framework/MASHotKey.h
new file mode 100644
index 0000000..1d267e4
--- /dev/null
+++ b/Framework/MASHotKey.h
@@ -0,0 +1,12 @@
+#import "MASShortcut.h"
+
+extern FourCharCode const MASHotKeySignature;
+
+@interface MASHotKey : NSObject
+
+@property(readonly) UInt32 carbonID;
+@property(copy) dispatch_block_t action;
+
++ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut;
+
+@end
diff --git a/Framework/MASHotKey.m b/Framework/MASHotKey.m
new file mode 100644
index 0000000..7886440
--- /dev/null
+++ b/Framework/MASHotKey.m
@@ -0,0 +1,44 @@
+#import "MASHotKey.h"
+
+FourCharCode const MASHotKeySignature = 'MASS';
+
+@interface MASHotKey ()
+@property(assign) EventHotKeyRef hotKeyRef;
+@property(assign) UInt32 carbonID;
+@end
+
+@implementation MASHotKey
+
+- (instancetype) initWithShortcut: (MASShortcut*) shortcut
+{
+ self = [super init];
+
+ static UInt32 CarbonHotKeyID = 0;
+
+ _carbonID = ++CarbonHotKeyID;
+ EventHotKeyID hotKeyID = { .signature = MASHotKeySignature, .id = _carbonID };
+
+ OSStatus status = RegisterEventHotKey([shortcut carbonKeyCode], [shortcut carbonFlags],
+ hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &_hotKeyRef);
+
+ if (status != noErr) {
+ return nil;
+ }
+
+ return self;
+}
+
++ (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut
+{
+ return [[self alloc] initWithShortcut:shortcut];
+}
+
+- (void) dealloc
+{
+ if (_hotKeyRef) {
+ UnregisterEventHotKey(_hotKeyRef);
+ _hotKeyRef = NULL;
+ }
+}
+
+@end
diff --git a/Framework/MASKeyCodes.h b/Framework/MASKeyCodes.h
new file mode 100644
index 0000000..8c1ce06
--- /dev/null
+++ b/Framework/MASKeyCodes.h
@@ -0,0 +1,42 @@
+#import <Carbon/Carbon.h>
+
+// These glyphs are missed in Carbon.h
+enum {
+ kMASShortcutGlyphEject = 0x23CF,
+ kMASShortcutGlyphClear = 0x2715,
+ kMASShortcutGlyphDeleteLeft = 0x232B,
+ kMASShortcutGlyphDeleteRight = 0x2326,
+ kMASShortcutGlyphLeftArrow = 0x2190,
+ kMASShortcutGlyphRightArrow = 0x2192,
+ kMASShortcutGlyphUpArrow = 0x2191,
+ kMASShortcutGlyphDownArrow = 0x2193,
+ kMASShortcutGlyphEscape = 0x238B,
+ kMASShortcutGlyphHelp = 0x003F,
+ kMASShortcutGlyphPageDown = 0x21DF,
+ kMASShortcutGlyphPageUp = 0x21DE,
+ kMASShortcutGlyphTabRight = 0x21E5,
+ kMASShortcutGlyphReturn = 0x2305,
+ kMASShortcutGlyphReturnR2L = 0x21A9,
+ kMASShortcutGlyphPadClear = 0x2327,
+ kMASShortcutGlyphNorthwestArrow = 0x2196,
+ kMASShortcutGlyphSoutheastArrow = 0x2198,
+} MASShortcutGlyph;
+
+NS_INLINE NSString* NSStringFromMASKeyCode(unsigned short ch)
+{
+ return [NSString stringWithFormat:@"%C", ch];
+}
+
+NS_INLINE NSUInteger MASPickCocoaModifiers(NSUInteger flags)
+{
+ return (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask));
+}
+
+NS_INLINE UInt32 MASCarbonModifiersFromCocoaModifiers(NSUInteger cocoaFlags)
+{
+ return
+ (cocoaFlags & NSCommandKeyMask ? cmdKey : 0)
+ | (cocoaFlags & NSAlternateKeyMask ? optionKey : 0)
+ | (cocoaFlags & NSControlKeyMask ? controlKey : 0)
+ | (cocoaFlags & NSShiftKeyMask ? shiftKey : 0);
+}
diff --git a/Framework/MASShortcut.h b/Framework/MASShortcut.h
new file mode 100644
index 0000000..3e1bedf
--- /dev/null
+++ b/Framework/MASShortcut.h
@@ -0,0 +1,70 @@
+#import "MASKeyCodes.h"
+
+/**
+ @brief A model class to hold a key combination.
+
+ This class just represents a combination of keys. It does not care if
+ the combination is valid or can be used as a hotkey, it doesn’t watch
+ the input system for the shortcut appearance, nor it does access user
+ defaults.
+*/
+@interface MASShortcut : NSObject <NSSecureCoding, NSCopying>
+
+/**
+ @brief The virtual key code for the keyboard key.
+
+ @Hardware independent, same as in NSEvent. Events.h in the HIToolbox
+ framework for a complete list, or Command-click this symbol: kVK_ANSI_A.
+*/
+@property (nonatomic, readonly) NSUInteger keyCode;
+
+/**
+ @brief Cocoa keyboard modifier flags.
+
+ Same as in NSEvent: NSCommandKeyMask, NSAlternateKeyMask, etc.
+*/
+@property (nonatomic, readonly) NSUInteger modifierFlags;
+
+/**
+ @brief Same as @p keyCode, just a different type.
+*/
+@property (nonatomic, readonly) UInt32 carbonKeyCode;
+
+/**
+ @brief Carbon modifier flags.
+
+ A bit sum of @p cmdKey, @p optionKey, etc.
+*/
+@property (nonatomic, readonly) UInt32 carbonFlags;
+
+/**
+ @brief A string representing the “key” part of a shortcut, like the “5” in “⌘5”.
+*/
+@property (nonatomic, readonly) NSString *keyCodeString;
+
+/**
+ @brief A key-code string used in key equivalent matching.
+
+ For precise meaning of “key equivalents” see the @p keyEquivalent
+ property of @p NSMenuItem. Here the string is used to support shortcut
+ validation (“is the shortcut already taken in this menu?”) and
+ for display in @p NSMenu.
+*/
+@property (nonatomic, readonly) NSString *keyCodeStringForKeyEquivalent;
+
+/**
+ @brief A string representing the shortcut modifiers, like the “⌘” in “⌘5”.
+*/
+@property (nonatomic, readonly) NSString *modifierFlagsString;
+
+- (instancetype)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
++ (instancetype)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
+
+/**
+ @brief Creates a new shortcut from an NSEvent object.
+
+ This is just a convenience initializer that reads the key code and modifiers from an NSEvent.
+*/
++ (instancetype)shortcutWithEvent:(NSEvent *)anEvent;
+
+@end
diff --git a/Framework/MASShortcut.m b/Framework/MASShortcut.m
new file mode 100644
index 0000000..e6fa63d
--- /dev/null
+++ b/Framework/MASShortcut.m
@@ -0,0 +1,241 @@
+#import "MASShortcut.h"
+
+static NSString *const MASShortcutKeyCode = @"KeyCode";
+static NSString *const MASShortcutModifierFlags = @"ModifierFlags";
+
+@implementation MASShortcut
+
+#pragma mark Initialization
+
+- (instancetype)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
+{
+ self = [super init];
+ if (self) {
+ _keyCode = code;
+ _modifierFlags = MASPickCocoaModifiers(flags);
+ }
+ return self;
+}
+
++ (instancetype)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
+{
+ return [[self alloc] initWithKeyCode:code modifierFlags:flags];
+}
+
++ (instancetype)shortcutWithEvent:(NSEvent *)event
+{
+ return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
+}
+
+#pragma mark Shortcut Accessors
+
+- (UInt32)carbonKeyCode
+{
+ return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
+}
+
+- (UInt32)carbonFlags
+{
+ return MASCarbonModifiersFromCocoaModifiers(self.modifierFlags);
+}
+
+- (NSString *)description
+{
+ return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString];
+}
+
+- (NSString *)keyCodeStringForKeyEquivalent
+{
+ NSString *keyCodeString = self.keyCodeString;
+ if (keyCodeString.length > 1) {
+ switch (self.keyCode) {
+ case kVK_F1: return NSStringFromMASKeyCode(0xF704);
+ case kVK_F2: return NSStringFromMASKeyCode(0xF705);
+ case kVK_F3: return NSStringFromMASKeyCode(0xF706);
+ case kVK_F4: return NSStringFromMASKeyCode(0xF707);
+ case kVK_F5: return NSStringFromMASKeyCode(0xF708);
+ case kVK_F6: return NSStringFromMASKeyCode(0xF709);
+ case kVK_F7: return NSStringFromMASKeyCode(0xF70a);
+ case kVK_F8: return NSStringFromMASKeyCode(0xF70b);
+ case kVK_F9: return NSStringFromMASKeyCode(0xF70c);
+ case kVK_F10: return NSStringFromMASKeyCode(0xF70d);
+ case kVK_F11: return NSStringFromMASKeyCode(0xF70e);
+ case kVK_F12: return NSStringFromMASKeyCode(0xF70f);
+ // From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
+ case kVK_F13: return NSStringFromMASKeyCode(0xF710);
+ case kVK_F14: return NSStringFromMASKeyCode(0xF711);
+ case kVK_F15: return NSStringFromMASKeyCode(0xF712);
+ case kVK_F16: return NSStringFromMASKeyCode(0xF713);
+ case kVK_F17: return NSStringFromMASKeyCode(0xF714);
+ case kVK_F18: return NSStringFromMASKeyCode(0xF715);
+ case kVK_F19: return NSStringFromMASKeyCode(0xF716);
+ case kVK_Space: return NSStringFromMASKeyCode(0x20);
+ default: return @"";
+ }
+ }
+ return keyCodeString.lowercaseString;
+}
+
+- (NSString *)keyCodeString
+{
+ // Some key codes don't have an equivalent
+ switch (self.keyCode) {
+ case NSNotFound: return @"";
+ case kVK_F1: return @"F1";
+ case kVK_F2: return @"F2";
+ case kVK_F3: return @"F3";
+ case kVK_F4: return @"F4";
+ case kVK_F5: return @"F5";
+ case kVK_F6: return @"F6";
+ case kVK_F7: return @"F7";
+ case kVK_F8: return @"F8";
+ case kVK_F9: return @"F9";
+ case kVK_F10: return @"F10";
+ case kVK_F11: return @"F11";
+ case kVK_F12: return @"F12";
+ case kVK_F13: return @"F13";
+ case kVK_F14: return @"F14";
+ case kVK_F15: return @"F15";
+ case kVK_F16: return @"F16";
+ case kVK_F17: return @"F17";
+ case kVK_F18: return @"F18";
+ case kVK_F19: return @"F19";
+ case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key");
+ case kVK_Escape: return NSStringFromMASKeyCode(kMASShortcutGlyphEscape);
+ case kVK_Delete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteLeft);
+ case kVK_ForwardDelete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteRight);
+ case kVK_LeftArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphLeftArrow);
+ case kVK_RightArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphRightArrow);
+ case kVK_UpArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphUpArrow);
+ case kVK_DownArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphDownArrow);
+ case kVK_Help: return NSStringFromMASKeyCode(kMASShortcutGlyphHelp);
+ case kVK_PageUp: return NSStringFromMASKeyCode(kMASShortcutGlyphPageUp);
+ case kVK_PageDown: return NSStringFromMASKeyCode(kMASShortcutGlyphPageDown);
+ case kVK_Tab: return NSStringFromMASKeyCode(kMASShortcutGlyphTabRight);
+ case kVK_Return: return NSStringFromMASKeyCode(kMASShortcutGlyphReturnR2L);
+
+ // Keypad
+ case kVK_ANSI_Keypad0: return @"0";
+ case kVK_ANSI_Keypad1: return @"1";
+ case kVK_ANSI_Keypad2: return @"2";
+ case kVK_ANSI_Keypad3: return @"3";
+ case kVK_ANSI_Keypad4: return @"4";
+ case kVK_ANSI_Keypad5: return @"5";
+ case kVK_ANSI_Keypad6: return @"6";
+ case kVK_ANSI_Keypad7: return @"7";
+ case kVK_ANSI_Keypad8: return @"8";
+ case kVK_ANSI_Keypad9: return @"9";
+ case kVK_ANSI_KeypadDecimal: return @".";
+ case kVK_ANSI_KeypadMultiply: return @"*";
+ case kVK_ANSI_KeypadPlus: return @"+";
+ case kVK_ANSI_KeypadClear: return NSStringFromMASKeyCode(kMASShortcutGlyphPadClear);
+ case kVK_ANSI_KeypadDivide: return @"/";
+ case kVK_ANSI_KeypadEnter: return NSStringFromMASKeyCode(kMASShortcutGlyphReturn);
+ case kVK_ANSI_KeypadMinus: return @"–";
+ case kVK_ANSI_KeypadEquals: return @"=";
+
+ // Hardcode
+ case 119: return NSStringFromMASKeyCode(kMASShortcutGlyphSoutheastArrow);
+ case 115: return NSStringFromMASKeyCode(kMASShortcutGlyphNorthwestArrow);
+ }
+
+ // Everything else should be printable so look it up in the current keyboard
+ OSStatus error = noErr;
+ NSString *keystroke = nil;
+ TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
+ if (inputSource) {
+ CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
+ if (layoutDataRef) {
+ UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef);
+ UniCharCount length = 0;
+ UniChar chars[256] = { 0 };
+ UInt32 deadKeyState = 0;
+ error = UCKeyTranslate(layoutData, (UInt16)self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
+ LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
+ sizeof(chars) / sizeof(UniChar), &length, chars);
+ keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @"");
+ }
+ CFRelease(inputSource);
+ }
+
+ // Validate keystroke
+ if (keystroke.length) {
+ static NSMutableCharacterSet *validChars = nil;
+ if (validChars == nil) {
+ validChars = [[NSMutableCharacterSet alloc] init];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
+ [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
+ }
+ for (NSUInteger i = 0, length = keystroke.length; i < length; i++) {
+ if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) {
+ keystroke = @"";
+ break;
+ }
+ }
+ }
+
+ // Finally, we've got a shortcut!
+ return keystroke.uppercaseString;
+}
+
+- (NSString *)modifierFlagsString
+{
+ unichar chars[4];
+ NSUInteger count = 0;
+ // These are in the same order as the menu manager shows them
+ if (self.modifierFlags & NSControlKeyMask) chars[count++] = kControlUnicode;
+ if (self.modifierFlags & NSAlternateKeyMask) chars[count++] = kOptionUnicode;
+ if (self.modifierFlags & NSShiftKeyMask) chars[count++] = kShiftUnicode;
+ if (self.modifierFlags & NSCommandKeyMask) chars[count++] = kCommandUnicode;
+ return (count ? [NSString stringWithCharacters:chars length:count] : @"");
+}
+
+#pragma mark NSObject
+
+- (BOOL) isEqual: (MASShortcut*) object
+{
+ return [object isKindOfClass:[self class]]
+ && (object.keyCode == self.keyCode)
+ && (object.modifierFlags == self.modifierFlags);
+}
+
+- (NSUInteger) hash
+{
+ return self.keyCode + self.modifierFlags;
+}
+
+#pragma mark NSCoding
+
+- (void)encodeWithCoder:(NSCoder *)coder
+{
+ [coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:MASShortcutKeyCode];
+ [coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags];
+}
+
+- (instancetype)initWithCoder:(NSCoder *)decoder
+{
+ self = [super init];
+ if (self) {
+ NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
+ _keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
+ _modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
+ }
+ return self;
+}
+
+#pragma mark NSSecureCoding
+
++ (BOOL)supportsSecureCoding
+{
+ return YES;
+}
+
+#pragma mark NSCopying
+
+- (instancetype) copyWithZone:(NSZone *)zone
+{
+ return [[self class] shortcutWithKeyCode:_keyCode modifierFlags:_modifierFlags];
+}
+
+@end
diff --git a/Framework/MASShortcutBinder.h b/Framework/MASShortcutBinder.h
new file mode 100644
index 0000000..1592e90
--- /dev/null
+++ b/Framework/MASShortcutBinder.h
@@ -0,0 +1,67 @@
+#import "MASShortcutMonitor.h"
+
+/**
+ @brief Binds actions to user defaults keys.
+
+ If you store shortcuts in user defaults (for example by binding
+ a @p MASShortcutView to user defaults), you can use this class to
+ connect an action directly to a user defaults key. If the shortcut
+ stored under the key changes, the action will get automatically
+ updated to the new one.
+
+ This class is mostly a wrapper around a @p MASShortcutMonitor. It
+ watches the changes in user defaults and updates the shortcut monitor
+ accordingly with the new shortcuts.
+*/
+@interface MASShortcutBinder : NSObject
+
+/**
+ @brief A convenience shared instance.
+
+ You may use it so that you don’t have to manage an instance by hand,
+ but it’s perfectly fine to allocate and use a separate instance instead.
+*/
++ (instancetype) sharedBinder;
+
+/**
+ @brief The underlying shortcut monitor.
+*/
+@property(strong) MASShortcutMonitor *shortcutMonitor;
+
+/**
+ @brief Binding options customizing the access to user defaults.
+
+ As an example, you can use @p NSValueTransformerNameBindingOption to customize
+ the storage format used for the shortcuts. By default the shortcuts are converted
+ from @p NSData (@p NSKeyedUnarchiveFromDataTransformerName). Note that if the
+ binder is to work with @p MASShortcutView, both object have to use the same storage
+ format.
+*/
+@property(copy) NSDictionary *bindingOptions;
+
+/**
+ @brief Binds given action to a shortcut stored under the given defaults key.
+
+ In other words, no matter what shortcut you store under the given key,
+ pressing it will always trigger the given action.
+*/
+- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action;
+
+/**
+ @brief Disconnect the binding between user defaults and action.
+
+ In other words, the shortcut stored under the given key will no longer trigger an action.
+*/
+- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName;
+
+/**
+ @brief Register default shortcuts in user defaults.
+
+ This is a convenience frontent to [NSUserDefaults registerDefaults].
+ The dictionary should contain a map of user defaults’ keys to appropriate
+ keyboard shortcuts. The shortcuts will be transformed according to
+ @p bindingOptions and registered using @p registerDefaults.
+*/
+- (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts;
+
+@end
diff --git a/Framework/MASShortcutBinder.m b/Framework/MASShortcutBinder.m
new file mode 100644
index 0000000..bf85c41
--- /dev/null
+++ b/Framework/MASShortcutBinder.m
@@ -0,0 +1,114 @@
+#import "MASShortcutBinder.h"
+#import "MASShortcut.h"
+
+@interface MASShortcutBinder ()
+@property(strong) NSMutableDictionary *actions;
+@property(strong) NSMutableDictionary *shortcuts;
+@end
+
+@implementation MASShortcutBinder
+
+#pragma mark Initialization
+
+- (id) init
+{
+ self = [super init];
+ [self setActions:[NSMutableDictionary dictionary]];
+ [self setShortcuts:[NSMutableDictionary dictionary]];
+ [self setShortcutMonitor:[MASShortcutMonitor sharedMonitor]];
+ [self setBindingOptions:@{NSValueTransformerNameBindingOption: NSKeyedUnarchiveFromDataTransformerName}];
+ return self;
+}
+
+- (void) dealloc
+{
+ for (NSString *bindingName in [_actions allKeys]) {
+ [self unbind:bindingName];
+ }
+}
+
++ (instancetype) sharedBinder
+{
+ static dispatch_once_t once;
+ static MASShortcutBinder *sharedInstance;
+ dispatch_once(&once, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+#pragma mark Registration
+
+- (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action
+{
+ [_actions setObject:[action copy] forKey:defaultsKeyName];
+ [self bind:defaultsKeyName toObject:[NSUserDefaultsController sharedUserDefaultsController]
+ withKeyPath:[@"values." stringByAppendingString:defaultsKeyName] options:_bindingOptions];
+}
+
+- (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName
+{
+ [_shortcutMonitor unregisterShortcut:[_shortcuts objectForKey:defaultsKeyName]];
+ [_shortcuts removeObjectForKey:defaultsKeyName];
+ [_actions removeObjectForKey:defaultsKeyName];
+ [self unbind:defaultsKeyName];
+}
+
+- (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts
+{
+ NSValueTransformer *transformer = [_bindingOptions valueForKey:NSValueTransformerBindingOption];
+ if (transformer == nil) {
+ NSString *transformerName = [_bindingOptions valueForKey:NSValueTransformerNameBindingOption];
+ if (transformerName) {
+ transformer = [NSValueTransformer valueTransformerForName:transformerName];
+ }
+ }
+
+ NSAssert(transformer != nil, @"Can’t register default shortcuts without a transformer.");
+
+ [defaultShortcuts enumerateKeysAndObjectsUsingBlock:^(NSString *defaultsKey, MASShortcut *shortcut, BOOL *stop) {
+ id value = [transformer reverseTransformedValue:shortcut];
+ [[NSUserDefaults standardUserDefaults] registerDefaults:@{defaultsKey:value}];
+ }];
+}
+
+#pragma mark Bindings
+
+- (BOOL) isRegisteredAction: (NSString*) name
+{
+ return !![_actions objectForKey:name];
+}
+
+- (id) valueForUndefinedKey: (NSString*) key
+{
+ return [self isRegisteredAction:key] ?
+ [_shortcuts objectForKey:key] :
+ [super valueForUndefinedKey:key];
+}
+
+- (void) setValue: (id) value forUndefinedKey: (NSString*) key
+{
+ if (![self isRegisteredAction:key]) {
+ [super setValue:value forUndefinedKey:key];
+ return;
+ }
+
+ MASShortcut *newShortcut = value;
+ MASShortcut *currentShortcut = [_shortcuts objectForKey:key];
+
+ // Unbind previous shortcut if any
+ if (currentShortcut != nil) {
+ [_shortcutMonitor unregisterShortcut:currentShortcut];
+ }
+
+ // Just deleting the old shortcut
+ if (newShortcut == nil) {
+ return;
+ }
+
+ // Bind new shortcut
+ [_shortcuts setObject:newShortcut forKey:key];
+ [_shortcutMonitor registerShortcut:newShortcut withAction:[_actions objectForKey:key]];
+}
+
+@end
diff --git a/Framework/MASShortcutBinderTests.m b/Framework/MASShortcutBinderTests.m
new file mode 100644
index 0000000..9f90a94
--- /dev/null
+++ b/Framework/MASShortcutBinderTests.m
@@ -0,0 +1,98 @@
+static NSString *const SampleDefaultsKey = @"sampleShortcut";
+
+@interface MASShortcutBinderTests : XCTestCase
+@property(strong) MASShortcutBinder *binder;
+@property(strong) MASShortcutMonitor *monitor;
+@property(strong) NSUserDefaults *defaults;
+@end
+
+@implementation MASShortcutBinderTests
+
+- (void) setUp
+{
+ [super setUp];
+ [self setBinder:[[MASShortcutBinder alloc] init]];
+ [self setMonitor:[_binder shortcutMonitor]];
+ [self setDefaults:[[NSUserDefaults alloc] init]];
+ [_defaults removeObjectForKey:SampleDefaultsKey];
+}
+
+- (void) tearDown
+{
+ [_monitor unregisterAllShortcuts];
+ [self setMonitor:nil];
+ [self setDefaults:nil];
+ [self setBinder:nil];
+ [super tearDown];
+}
+
+- (void) testInitialValueReading
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Pass the initial shortcut from defaults to shortcut monitor.");
+}
+
+- (void) testValueChangeReading
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Pass the shortcut from defaults to shortcut monitor after defaults change.");
+}
+
+- (void) testValueClearing
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_defaults removeObjectForKey:SampleDefaultsKey];
+ XCTAssertFalse([_monitor isShortcutRegistered:shortcut],
+ @"Unregister shortcut from monitor after value is cleared from defaults.");
+}
+
+- (void) testBindingRemoval
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_binder breakBindingWithDefaultsKey:SampleDefaultsKey];
+ XCTAssertFalse([_monitor isShortcutRegistered:shortcut],
+ @"Unregister shortcut from monitor after binding was removed.");
+}
+
+- (void) testRebinding
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1];
+ [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ [_binder breakBindingWithDefaultsKey:SampleDefaultsKey];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Bind after unbinding.");
+}
+
+- (void) testTransformerDeserialization
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576];
+ NSDictionary *storedShortcut = @{@"keyCode": @5, @"modifierFlags": @1048576};
+ [_defaults setObject:storedShortcut forKey:SampleDefaultsKey];
+ [_binder setBindingOptions:@{NSValueTransformerBindingOption:[MASDictionaryTransformer new]}];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Deserialize shortcut from user defaults using a custom transformer.");
+}
+
+- (void) testDefaultShortcuts
+{
+ MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576];
+ [_binder registerDefaultShortcuts:@{SampleDefaultsKey: shortcut}];
+ [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}];
+ XCTAssertTrue([_monitor isShortcutRegistered:shortcut],
+ @"Bind shortcut using a default value.");
+}
+
+@end
diff --git a/Framework/MASShortcutMonitor.h b/Framework/MASShortcutMonitor.h
new file mode 100644
index 0000000..609686a
--- /dev/null
+++ b/Framework/MASShortcutMonitor.h
@@ -0,0 +1,27 @@
+#import "MASShortcut.h"
+
+/**
+ @brief Executes action when a shortcut is pressed.
+
+ There can only be one instance of this class, otherwise things
+ will probably not work. (There’s a Carbon event handler inside
+ and there can only be one Carbon event handler of a given type.)
+*/
+@interface MASShortcutMonitor : NSObject
+
+- (instancetype) init __unavailable;
++ (instancetype) sharedMonitor;
+
+/**
+ @brief Register a shortcut along with an action.
+
+ Attempting to insert an already registered shortcut probably won’t work.
+ It may burn your house or cut your fingers. You have been warned.
+*/
+- (void) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action;
+- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut;
+
+- (void) unregisterShortcut: (MASShortcut*) shortcut;
+- (void) unregisterAllShortcuts;
+
+@end
diff --git a/Framework/MASShortcutMonitor.m b/Framework/MASShortcutMonitor.m
new file mode 100644
index 0000000..cb89ce1
--- /dev/null
+++ b/Framework/MASShortcutMonitor.m
@@ -0,0 +1,101 @@
+#import "MASShortcutMonitor.h"
+#import "MASHotKey.h"
+
+@interface MASShortcutMonitor ()
+@property(assign) EventHandlerRef eventHandlerRef;
+@property(strong) NSMutableDictionary *hotKeys;
+@end
+
+static OSStatus MASCarbonEventCallback(EventHandlerCallRef, EventRef, void*);
+
+@implementation MASShortcutMonitor
+
+#pragma mark Initialization
+
+- (instancetype) init
+{
+ self = [super init];
+ [self setHotKeys:[NSMutableDictionary dictionary]];
+ EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed };
+ OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), MASCarbonEventCallback,
+ 1, &hotKeyPressedSpec, (__bridge void*)self, &_eventHandlerRef);
+ if (status != noErr) {
+ return nil;
+ }
+ return self;
+}
+
+- (void) dealloc
+{
+ if (_eventHandlerRef) {
+ RemoveEventHandler(_eventHandlerRef);
+ _eventHandlerRef = NULL;
+ }
+}
+
++ (instancetype) sharedMonitor
+{
+ static dispatch_once_t once;
+ static MASShortcutMonitor *sharedInstance;
+ dispatch_once(&once, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+#pragma mark Registration
+
+- (void) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action
+{
+ MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut:shortcut];
+ [hotKey setAction:action];
+ [_hotKeys setObject:hotKey forKey:shortcut];
+}
+
+- (void) unregisterShortcut: (MASShortcut*) shortcut
+{
+ [_hotKeys removeObjectForKey:shortcut];
+}
+
+- (void) unregisterAllShortcuts
+{
+ [_hotKeys removeAllObjects];
+}
+
+- (BOOL) isShortcutRegistered: (MASShortcut*) shortcut
+{
+ return !![_hotKeys objectForKey:shortcut];
+}
+
+#pragma mark Event Handling
+
+- (void) handleEvent: (EventRef) event
+{
+ if (GetEventClass(event) != kEventClassKeyboard) {
+ return;
+ }
+
+ EventHotKeyID hotKeyID;
+ OSStatus status = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
+ if (status != noErr || hotKeyID.signature != MASHotKeySignature) {
+ return;
+ }
+
+ [_hotKeys enumerateKeysAndObjectsUsingBlock:^(MASShortcut *shortcut, MASHotKey *hotKey, BOOL *stop) {
+ if (hotKeyID.id == [hotKey carbonID]) {
+ if ([hotKey action]) {
+ dispatch_async(dispatch_get_main_queue(), [hotKey action]);
+ }
+ *stop = YES;
+ }
+ }];
+}
+
+@end
+
+static OSStatus MASCarbonEventCallback(EventHandlerCallRef _, EventRef event, void *context)
+{
+ MASShortcutMonitor *dispatcher = (__bridge id)context;
+ [dispatcher handleEvent:event];
+ return noErr;
+}
diff --git a/Framework/MASShortcutTests.m b/Framework/MASShortcutTests.m
new file mode 100644
index 0000000..28eab56
--- /dev/null
+++ b/Framework/MASShortcutTests.m
@@ -0,0 +1,26 @@
+@interface MASShortcutTests : XCTestCase
+@end
+
+@implementation MASShortcutTests
+
+- (void) testEquality
+{
+ MASShortcut *keyA = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSControlKeyMask];
+ MASShortcut *keyB = [MASShortcut shortcutWithKeyCode:2 modifierFlags:NSControlKeyMask];
+ MASShortcut *keyC = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSAlternateKeyMask];
+ MASShortcut *keyD = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSControlKeyMask];
+ XCTAssertTrue([keyA isEqual:keyA], @"Shortcut is equal to itself.");
+ XCTAssertTrue([keyA isEqual:[keyA copy]], @"Shortcut is equal to its copy.");
+ XCTAssertFalse([keyA isEqual:keyB], @"Shortcuts not equal when key codes differ.");
+ XCTAssertFalse([keyA isEqual:keyC], @"Shortcuts not equal when modifier flags differ.");
+ XCTAssertTrue([keyA isEqual:keyD], @"Shortcuts are equal when key codes and modifiers are.");
+ XCTAssertFalse([keyA isEqual:@"foo"], @"Shortcut not equal to an object of a different class.");
+}
+
+- (void) testShortcutRecorderCompatibility
+{
+ MASShortcut *key = [MASShortcut shortcutWithKeyCode:87 modifierFlags:1048576];
+ XCTAssertEqualObjects([key description], @"⌘5", @"Basic compatibility with the keycode & modifier combination used by Shortcut Recorder.");
+}
+
+@end
diff --git a/Framework/MASShortcutValidator.h b/Framework/MASShortcutValidator.h
new file mode 100644
index 0000000..cc5f816
--- /dev/null
+++ b/Framework/MASShortcutValidator.h
@@ -0,0 +1,15 @@
+#import "MASShortcut.h"
+
+@interface MASShortcutValidator : NSObject
+
+// The following API enable hotkeys with the Option key as the only modifier
+// For example, Option-G will not generate © and Option-R will not paste ®
+@property(assign) BOOL allowAnyShortcutWithOptionModifier;
+
++ (instancetype) sharedValidator;
+
+- (BOOL) isShortcutValid: (MASShortcut*) shortcut;
+- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation;
+- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation;
+
+@end
diff --git a/Framework/MASShortcutValidator.m b/Framework/MASShortcutValidator.m
new file mode 100644
index 0000000..47dd700
--- /dev/null
+++ b/Framework/MASShortcutValidator.m
@@ -0,0 +1,111 @@
+#import "MASShortcutValidator.h"
+
+@implementation MASShortcutValidator
+
++ (instancetype) sharedValidator
+{
+ static dispatch_once_t once;
+ static MASShortcutValidator *sharedInstance;
+ dispatch_once(&once, ^{
+ sharedInstance = [[self alloc] init];
+ });
+ return sharedInstance;
+}
+
+- (BOOL) isShortcutValid: (MASShortcut*) shortcut
+{
+ NSUInteger keyCode = [shortcut keyCode];
+ NSUInteger modifiers = [shortcut modifierFlags];
+
+ // Allow any function key with any combination of modifiers
+ BOOL includesFunctionKey = ((keyCode == kVK_F1) || (keyCode == kVK_F2) || (keyCode == kVK_F3) || (keyCode == kVK_F4) ||
+ (keyCode == kVK_F5) || (keyCode == kVK_F6) || (keyCode == kVK_F7) || (keyCode == kVK_F8) ||
+ (keyCode == kVK_F9) || (keyCode == kVK_F10) || (keyCode == kVK_F11) || (keyCode == kVK_F12) ||
+ (keyCode == kVK_F13) || (keyCode == kVK_F14) || (keyCode == kVK_F15) || (keyCode == kVK_F16) ||
+ (keyCode == kVK_F17) || (keyCode == kVK_F18) || (keyCode == kVK_F19) || (keyCode == kVK_F20));
+ if (includesFunctionKey) return YES;
+
+ // Do not allow any other key without modifiers
+ BOOL hasModifierFlags = (modifiers > 0);
+ if (!hasModifierFlags) return NO;
+
+ // Allow any hotkey containing Control or Command modifier
+ BOOL includesCommand = ((modifiers & NSCommandKeyMask) > 0);
+ BOOL includesControl = ((modifiers & NSControlKeyMask) > 0);
+ if (includesCommand || includesControl) return YES;
+
+ // Allow Option key only in selected cases
+ BOOL includesOption = ((modifiers & NSAlternateKeyMask) > 0);
+ if (includesOption) {
+
+ // Always allow Option-Space and Option-Escape because they do not have any bind system commands
+ if ((keyCode == kVK_Space) || (keyCode == kVK_Escape)) return YES;
+
+ // Allow Option modifier with any key even if it will break the system binding
+ if (_allowAnyShortcutWithOptionModifier) return YES;
+ }
+
+ // The hotkey does not have any modifiers or violates system bindings
+ return NO;
+}
+
+- (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation
+{
+ NSString *keyEquivalent = [shortcut keyCodeStringForKeyEquivalent];
+ NSUInteger flags = [shortcut modifierFlags];
+
+ for (NSMenuItem *menuItem in menu.itemArray) {
+ if (menuItem.hasSubmenu && [self isShortcut:shortcut alreadyTakenInMenu:[menuItem submenu] explanation:explanation]) return YES;
+
+ BOOL equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask) == flags);
+ BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent];
+
+ // Check if the cases are different, we know ours is lower and that shift is included in our modifiers
+ // If theirs is capitol, we need to add shift to their modifiers
+ if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) {
+ equalFlags = (MASPickCocoaModifiers(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
+ }
+
+ if (equalFlags && equalHotkeyLowercase) {
+ if (explanation) {
+ *explanation = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.",
+ @"Message for alert when shortcut is already used");
+ *explanation = [NSString stringWithFormat:*explanation, menuItem.title];
+ }
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation
+{
+ CFArrayRef globalHotKeys;
+ if (CopySymbolicHotKeys(&globalHotKeys) == noErr) {
+
+ // Enumerate all global hotkeys and check if any of them matches current shortcut
+ for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) {
+ CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i);
+ CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode);
+ CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers);
+ CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled);
+
+ if (([(__bridge NSNumber *)code unsignedIntegerValue] == [shortcut keyCode]) &&
+ ([(__bridge NSNumber *)flags unsignedIntegerValue] == [shortcut carbonFlags]) &&
+ ([(__bridge NSNumber *)enabled boolValue])) {
+
+ if (explanation) {
+ *explanation = NSLocalizedString(@"This combination cannot be used because it is already used by a system-wide "
+ @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts "
+ @"can be changed in the Keyboard & Mouse panel in System Preferences.",
+ @"Message for alert when shortcut is already used by the system");
+ }
+ return YES;
+ }
+ }
+ CFRelease(globalHotKeys);
+ }
+ return [self isShortcut:shortcut alreadyTakenInMenu:[NSApp mainMenu] explanation:explanation];
+}
+
+@end
diff --git a/Framework/MASShortcutView+Bindings.h b/Framework/MASShortcutView+Bindings.h
new file mode 100644
index 0000000..b0148e7
--- /dev/null
+++ b/Framework/MASShortcutView+Bindings.h
@@ -0,0 +1,25 @@
+#import "MASShortcutView.h"
+
+/**
+ @brief A simplified interface to bind the recorder value to user defaults.
+
+ You can bind the @p shortcutValue to user defaults using the standard
+ @p bind:toObject:withKeyPath:options: call, but since that’s a lot to type
+ and read, here’s a simpler option.
+
+ Setting the @p associatedUserDefaultsKey binds the view’s shortcut value
+ to the given user defaults key. You can supply a value transformer to convert
+ values between user defaults and @p MASShortcut. If you don’t supply
+ a transformer, the @p NSUnarchiveFromDataTransformerName will be used
+ automatically.
+
+ Set @p associatedUserDefaultsKey to @p nil to disconnect the binding.
+*/
+@interface MASShortcutView (Bindings)
+
+@property(copy) NSString *associatedUserDefaultsKey;
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer;
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName;
+
+@end
diff --git a/Framework/MASShortcutView+Bindings.m b/Framework/MASShortcutView+Bindings.m
new file mode 100644
index 0000000..54c5111
--- /dev/null
+++ b/Framework/MASShortcutView+Bindings.m
@@ -0,0 +1,50 @@
+#import "MASShortcutView+Bindings.h"
+
+@implementation MASShortcutView (Bindings)
+
+- (NSString*) associatedUserDefaultsKey
+{
+ NSDictionary* bindingInfo = [self infoForBinding:MASShortcutBinding];
+ if (bindingInfo != nil) {
+ NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
+ NSString *key = [keyPath stringByReplacingOccurrencesOfString:@"values." withString:@""];
+ return key;
+ } else {
+ return nil;
+ }
+}
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer
+{
+ // Break previous binding if any
+ NSString *currentKey = [self associatedUserDefaultsKey];
+ if (currentKey != nil) {
+ [self unbind:currentKey];
+ }
+
+ // Stop if the new binding is nil
+ if (newKey == nil) {
+ return;
+ }
+
+ NSDictionary *options = transformer ?
+ @{NSValueTransformerBindingOption:transformer} :
+ nil;
+
+ [self bind:MASShortcutBinding
+ toObject:[NSUserDefaultsController sharedUserDefaultsController]
+ withKeyPath:[@"values." stringByAppendingString:newKey]
+ options:options];
+}
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName
+{
+ [self setAssociatedUserDefaultsKey:newKey withTransformer:[NSValueTransformer valueTransformerForName:transformerName]];
+}
+
+- (void) setAssociatedUserDefaultsKey: (NSString*) newKey
+{
+ [self setAssociatedUserDefaultsKey:newKey withTransformerName:NSKeyedUnarchiveFromDataTransformerName];
+}
+
+@end
diff --git a/Framework/MASShortcutView.h b/Framework/MASShortcutView.h
new file mode 100644
index 0000000..166be44
--- /dev/null
+++ b/Framework/MASShortcutView.h
@@ -0,0 +1,24 @@
+@class MASShortcut, MASShortcutValidator;
+
+extern NSString *const MASShortcutBinding;
+
+typedef enum {
+ MASShortcutViewStyleDefault = 0, // Height = 19 px
+ MASShortcutViewStyleTexturedRect, // Height = 25 px
+ MASShortcutViewStyleRounded, // Height = 43 px
+ MASShortcutViewStyleFlat
+} MASShortcutViewStyle;
+
+@interface MASShortcutView : NSView
+
+@property (nonatomic, strong) MASShortcut *shortcutValue;
+@property (nonatomic, strong) MASShortcutValidator *shortcutValidator;
+@property (nonatomic, getter = isRecording) BOOL recording;
+@property (nonatomic, getter = isEnabled) BOOL enabled;
+@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
+@property (nonatomic, assign) MASShortcutViewStyle style;
+
+/// Returns custom class for drawing control.
++ (Class)shortcutCellClass;
+
+@end
diff --git a/MASShortcutView.m b/Framework/MASShortcutView.m
index dd75729..aace67e 100644
--- a/MASShortcutView.m
+++ b/Framework/MASShortcutView.m
@@ -1,5 +1,7 @@
#import "MASShortcutView.h"
-#import "MASShortcut.h"
+#import "MASShortcutValidator.h"
+
+NSString *const MASShortcutBinding = @"shortcutValue";
#define HINT_BUTTON_WIDTH 23.0
#define BUTTON_FONT_SIZE 11.0
@@ -23,14 +25,6 @@
NSTrackingArea *_hintArea;
}
-@synthesize enabled = _enabled;
-@synthesize hinting = _hinting;
-@synthesize shortcutValue = _shortcutValue;
-@synthesize shortcutPlaceholder = _shortcutPlaceholder;
-@synthesize shortcutValueChange = _shortcutValueChange;
-@synthesize recording = _recording;
-@synthesize appearance = _appearance;
-
#pragma mark -
+ (Class)shortcutCellClass
@@ -61,6 +55,7 @@
_shortcutCell = [[[self.class shortcutCellClass] alloc] init];
_shortcutCell.buttonType = NSPushOnPushOffButton;
_shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:BUTTON_FONT_SIZE];
+ _shortcutValidator = [MASShortcutValidator sharedValidator];
_enabled = YES;
[self resetShortcutCellStyle];
}
@@ -83,10 +78,10 @@
}
}
-- (void)setAppearance:(MASShortcutViewAppearance)appearance
+- (void)setStyle:(MASShortcutViewStyle)newStyle
{
- if (_appearance != appearance) {
- _appearance = appearance;
+ if (_style != newStyle) {
+ _style = newStyle;
[self resetShortcutCellStyle];
[self setNeedsDisplay:YES];
}
@@ -94,20 +89,20 @@
- (void)resetShortcutCellStyle
{
- switch (_appearance) {
- case MASShortcutViewAppearanceDefault: {
+ switch (_style) {
+ case MASShortcutViewStyleDefault: {
_shortcutCell.bezelStyle = NSRoundRectBezelStyle;
break;
}
- case MASShortcutViewAppearanceTexturedRect: {
+ case MASShortcutViewStyleTexturedRect: {
_shortcutCell.bezelStyle = NSTexturedRoundedBezelStyle;
break;
}
- case MASShortcutViewAppearanceRounded: {
+ case MASShortcutViewStyleRounded: {
_shortcutCell.bezelStyle = NSRoundedBezelStyle;
break;
}
- case MASShortcutViewAppearanceFlat: {
+ case MASShortcutViewStyleFlat: {
self.wantsLayer = YES;
_shortcutCell.backgroundColor = [NSColor clearColor];
_shortcutCell.bordered = NO;
@@ -143,6 +138,7 @@
_shortcutValue = shortcutValue;
[self resetToolTips];
[self setNeedsDisplay:YES];
+ [self propagateValue:shortcutValue forBinding:@"shortcutValue"];
if (self.shortcutValueChange) {
self.shortcutValueChange(self);
@@ -169,20 +165,20 @@
_shortcutCell.state = state;
_shortcutCell.enabled = self.enabled;
- switch (_appearance) {
- case MASShortcutViewAppearanceDefault: {
+ switch (_style) {
+ case MASShortcutViewStyleDefault: {
[_shortcutCell drawWithFrame:frame inView:self];
break;
}
- case MASShortcutViewAppearanceTexturedRect: {
+ case MASShortcutViewStyleTexturedRect: {
[_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
break;
}
- case MASShortcutViewAppearanceRounded: {
+ case MASShortcutViewStyleRounded: {
[_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, 1.0) inView:self];
break;
}
- case MASShortcutViewAppearanceFlat: {
+ case MASShortcutViewStyleFlat: {
[_shortcutCell drawWithFrame:frame inView:self];
break;
}
@@ -192,7 +188,7 @@
- (void)drawRect:(CGRect)dirtyRect
{
if (self.shortcutValue) {
- [self drawInRect:self.bounds withTitle:MASShortcutChar(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphDeleteLeft)
+ [self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(self.recording ? kMASShortcutGlyphEscape : kMASShortcutGlyphDeleteLeft)
alignment:NSRightTextAlignment state:NSOffState];
CGRect shortcutRect;
@@ -209,7 +205,7 @@
else {
if (self.recording)
{
- [self drawInRect:self.bounds withTitle:MASShortcutChar(kMASShortcutGlyphEscape) alignment:NSRightTextAlignment state:NSOffState];
+ [self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(kMASShortcutGlyphEscape) alignment:NSRightTextAlignment state:NSOffState];
CGRect shortcutRect;
[self getShortcutRect:&shortcutRect hintRect:NULL];
@@ -234,10 +230,10 @@
{
CGRect shortcutRect, hintRect;
CGFloat hintButtonWidth = HINT_BUTTON_WIDTH;
- switch (self.appearance) {
- case MASShortcutViewAppearanceTexturedRect: hintButtonWidth += 2.0; break;
- case MASShortcutViewAppearanceRounded: hintButtonWidth += 3.0; break;
- case MASShortcutViewAppearanceFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - BUTTON_FONT_SIZE); break;
+ switch (self.style) {
+ case MASShortcutViewStyleTexturedRect: hintButtonWidth += 2.0; break;
+ case MASShortcutViewStyleRounded: hintButtonWidth += 3.0; break;
+ case MASShortcutViewStyleFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - BUTTON_FONT_SIZE); break;
default: break;
}
CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge);
@@ -377,40 +373,45 @@ void *kUserDataHint = &kUserDataHint;
NSEventMask eventMask = (NSKeyDownMask | NSFlagsChangedMask);
eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) {
+ // Create a shortcut from the event
MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event];
- if ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete)) {
- // Delete shortcut
+
+ // If the shortcut is a plain Delete or Backspace, clear the current shortcut and cancel recording
+ if (!shortcut.modifierFlags && ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete))) {
weakSelf.shortcutValue = nil;
weakSelf.recording = NO;
event = nil;
}
- else if (shortcut.keyCode == kVK_Escape && !shortcut.modifierFlags) {
- // Cancel recording
+
+ // If the shortcut is a plain Esc, cancel recording
+ else if (!shortcut.modifierFlags && shortcut.keyCode == kVK_Escape) {
weakSelf.recording = NO;
event = nil;
}
- else if (shortcut.shouldBypass) {
- // Command + W, Command + Q, ESC should deactivate recorder
+
+ // If the shortcut is Cmd-W or Cmd-Q, cancel recording and pass the event through
+ else if ((shortcut.modifierFlags == NSCommandKeyMask) && (shortcut.keyCode == kVK_ANSI_W || shortcut.keyCode == kVK_ANSI_Q)) {
weakSelf.recording = NO;
}
+
else {
// Verify possible shortcut
if (shortcut.keyCodeString.length > 0) {
- if (shortcut.valid) {
+ if ([_shortcutValidator isShortcutValid:shortcut]) {
// Verify that shortcut is not used
- NSError *error = nil;
- if ([shortcut isTakenError:&error]) {
+ NSString *explanation = nil;
+ if ([_shortcutValidator isShortcutAlreadyTakenBySystem:shortcut explanation:&explanation]) {
// Prevent cancel of recording when Alert window is key
[weakSelf activateResignObserver:NO];
[weakSelf activateEventMonitoring:NO];
NSString *format = NSLocalizedString(@"The key combination %@ cannot be used",
@"Title for alert when shortcut is already used");
- NSAlert *alert = [[NSAlert alloc] init];
+ NSAlert* alert = [[NSAlert alloc]init];
alert.alertStyle = NSCriticalAlertStyle;
- alert.informativeText = [NSString stringWithFormat:format, shortcut];
- alert.messageText = error.localizedDescription;
+ alert.informativeText = explanation;
+ alert.messageText = [NSString stringWithFormat:format, shortcut];
[alert addButtonWithTitle:NSLocalizedString(@"OK", @"Alert button when shortcut is already used")];
-
+
[alert runModal];
weakSelf.shortcutPlaceholder = nil;
[weakSelf activateResignObserver:YES];
@@ -460,4 +461,51 @@ void *kUserDataHint = &kUserDataHint;
}
}
+#pragma mark Bindings
+
+// http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/
+-(void) propagateValue:(id)value forBinding:(NSString*)binding;
+{
+ NSParameterAssert(binding != nil);
+
+ //WARNING: bindingInfo contains NSNull, so it must be accounted for
+ NSDictionary* bindingInfo = [self infoForBinding:binding];
+ if(!bindingInfo)
+ return; //there is no binding
+
+ //apply the value transformer, if one has been set
+ NSDictionary* bindingOptions = [bindingInfo objectForKey:NSOptionsKey];
+ if(bindingOptions){
+ NSValueTransformer* transformer = [bindingOptions valueForKey:NSValueTransformerBindingOption];
+ if(!transformer || (id)transformer == [NSNull null]){
+ NSString* transformerName = [bindingOptions valueForKey:NSValueTransformerNameBindingOption];
+ if(transformerName && (id)transformerName != [NSNull null]){
+ transformer = [NSValueTransformer valueTransformerForName:transformerName];
+ }
+ }
+
+ if(transformer && (id)transformer != [NSNull null]){
+ if([[transformer class] allowsReverseTransformation]){
+ value = [transformer reverseTransformedValue:value];
+ } else {
+ NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", binding, __PRETTY_FUNCTION__);
+ }
+ }
+ }
+
+ id boundObject = [bindingInfo objectForKey:NSObservedObjectKey];
+ if(!boundObject || boundObject == [NSNull null]){
+ NSLog(@"ERROR: NSObservedObjectKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__);
+ return;
+ }
+
+ NSString* boundKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
+ if(!boundKeyPath || (id)boundKeyPath == [NSNull null]){
+ NSLog(@"ERROR: NSObservedKeyPathKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__);
+ return;
+ }
+
+ [boundObject setValue:value forKeyPath:boundKeyPath];
+}
+
@end
diff --git a/Framework/Prefix.pch b/Framework/Prefix.pch
new file mode 100644
index 0000000..3e71c31
--- /dev/null
+++ b/Framework/Prefix.pch
@@ -0,0 +1,2 @@
+#import <AppKit/AppKit.h>
+#import <Carbon/Carbon.h> \ No newline at end of file
diff --git a/Framework/Shortcut.h b/Framework/Shortcut.h
new file mode 100644
index 0000000..e131395
--- /dev/null
+++ b/Framework/Shortcut.h
@@ -0,0 +1,7 @@
+#import "MASShortcut.h"
+#import "MASShortcutValidator.h"
+#import "MASShortcutMonitor.h"
+#import "MASShortcutBinder.h"
+#import "MASDictionaryTransformer.h"
+#import "MASShortcutView.h"
+#import "MASShortcutView+Bindings.h" \ No newline at end of file
diff --git a/MASShortcut+Monitoring.h b/MASShortcut+Monitoring.h
deleted file mode 100644
index aa8f224..0000000
--- a/MASShortcut+Monitoring.h
+++ /dev/null
@@ -1,8 +0,0 @@
-#import "MASShortcut.h"
-
-@interface MASShortcut (Monitoring)
-
-+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler;
-+ (void)removeGlobalHotkeyMonitor:(id)monitor;
-
-@end
diff --git a/MASShortcut+Monitoring.m b/MASShortcut+Monitoring.m
deleted file mode 100644
index b6750b4..0000000
--- a/MASShortcut+Monitoring.m
+++ /dev/null
@@ -1,165 +0,0 @@
-#import "MASShortcut+Monitoring.h"
-
-NSMutableDictionary *MASRegisteredHotKeys(void);
-BOOL InstallCommonEventHandler(void);
-BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey);
-void UninstallEventHandler(void);
-
-#pragma mark -
-
-@interface MASShortcutHotKey : NSObject
-
-@property (nonatomic, readonly) MASShortcut *shortcut;
-@property (nonatomic, readonly, copy) void (^handler)();
-@property (nonatomic, readonly) EventHotKeyRef carbonHotKey;
-@property (nonatomic, readonly) UInt32 carbonHotKeyID;
-
-- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler;
-
-@end
-
-#pragma mark -
-
-@implementation MASShortcut (Monitoring)
-
-+ (id)addGlobalHotkeyMonitorWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
-{
- NSString *monitor = [NSString stringWithFormat:@"%@", shortcut.description];
- if ([MASRegisteredHotKeys() objectForKey:monitor]) return nil;
-
- MASShortcutHotKey *hotKey = [[MASShortcutHotKey alloc] initWithShortcut:shortcut handler:handler];
- if (hotKey == nil) return nil;
-
- [MASRegisteredHotKeys() setObject:hotKey forKey:monitor];
- return monitor;
-}
-
-+ (void)removeGlobalHotkeyMonitor:(id)monitor
-{
- if (monitor == nil) return;
- NSMutableDictionary *registeredHotKeys = MASRegisteredHotKeys();
- [registeredHotKeys removeObjectForKey:monitor];
- if (registeredHotKeys.count == 0) {
- UninstallEventHandler();
- }
-}
-
-@end
-
-#pragma mark -
-
-@implementation MASShortcutHotKey
-
-@synthesize carbonHotKeyID = _carbonHotKeyID;
-@synthesize handler = _handler;
-@synthesize shortcut = _shortcut;
-@synthesize carbonHotKey = _carbonHotKey;
-
-#pragma mark -
-
-- (id)initWithShortcut:(MASShortcut *)shortcut handler:(void (^)())handler
-{
- self = [super init];
- if (self) {
- _shortcut = shortcut;
- _handler = [handler copy];
-
- if (!InstallHotkeyWithShortcut(shortcut, &_carbonHotKeyID, &_carbonHotKey))
- self = nil;
- }
- return self;
-}
-
-- (void)dealloc
-{
- [self uninstallExisitingHotKey];
-}
-
-- (void)uninstallExisitingHotKey
-{
- if (_carbonHotKey) {
- UnregisterEventHotKey(_carbonHotKey);
- _carbonHotKey = NULL;
- }
-}
-
-@end
-
-#pragma mark - Carbon magic
-
-NSMutableDictionary *MASRegisteredHotKeys()
-{
- static NSMutableDictionary *shared = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- shared = [NSMutableDictionary dictionary];
- });
- return shared;
-}
-
-#pragma mark -
-
-FourCharCode const kMASShortcutSignature = 'MASS';
-
-BOOL InstallHotkeyWithShortcut(MASShortcut *shortcut, UInt32 *outCarbonHotKeyID, EventHotKeyRef *outCarbonHotKey)
-{
- if ((shortcut == nil) || !InstallCommonEventHandler()) return NO;
-
- static UInt32 sCarbonHotKeyID = 0;
- EventHotKeyID hotKeyID = { .signature = kMASShortcutSignature, .id = ++ sCarbonHotKeyID };
- EventHotKeyRef carbonHotKey = NULL;
- if (RegisterEventHotKey(shortcut.carbonKeyCode, shortcut.carbonFlags, hotKeyID, GetEventDispatcherTarget(), kEventHotKeyExclusive, &carbonHotKey) != noErr) {
- return NO;
- }
-
- if (outCarbonHotKeyID) *outCarbonHotKeyID = hotKeyID.id;
- if (outCarbonHotKey) *outCarbonHotKey = carbonHotKey;
- return YES;
-}
-
-static OSStatus CarbonCallback(EventHandlerCallRef inHandlerCallRef, EventRef inEvent, void *inUserData)
-{
- if (GetEventClass(inEvent) != kEventClassKeyboard) return noErr;
-
- EventHotKeyID hotKeyID;
- OSStatus status = GetEventParameter(inEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID);
- if (status != noErr) return status;
-
- if (hotKeyID.signature != kMASShortcutSignature) return noErr;
-
- [MASRegisteredHotKeys() enumerateKeysAndObjectsUsingBlock:^(id key, MASShortcutHotKey *hotKey, BOOL *stop) {
- if (hotKeyID.id == hotKey.carbonHotKeyID) {
- if (hotKey.handler) {
- hotKey.handler();
- }
- *stop = YES;
- }
- }];
-
- return noErr;
-}
-
-#pragma mark -
-
-static EventHandlerRef sEventHandler = NULL;
-
-BOOL InstallCommonEventHandler()
-{
- if (sEventHandler == NULL) {
- EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed };
- OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), CarbonCallback, 1, &hotKeyPressedSpec, NULL, &sEventHandler);
- if (status != noErr) {
- sEventHandler = NULL;
- return NO;
- }
- }
- return YES;
-}
-
-void UninstallEventHandler()
-{
- if (sEventHandler) {
- RemoveEventHandler(sEventHandler);
- sEventHandler = NULL;
- }
-}
diff --git a/MASShortcut+UserDefaults.h b/MASShortcut+UserDefaults.h
deleted file mode 100644
index 9f2ecb9..0000000
--- a/MASShortcut+UserDefaults.h
+++ /dev/null
@@ -1,9 +0,0 @@
-#import "MASShortcut.h"
-
-@interface MASShortcut (UserDefaults)
-
-+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
-+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey;
-+ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey;
-
-@end
diff --git a/MASShortcut+UserDefaults.m b/MASShortcut+UserDefaults.m
deleted file mode 100644
index 94b035d..0000000
--- a/MASShortcut+UserDefaults.m
+++ /dev/null
@@ -1,98 +0,0 @@
-#import "MASShortcut+UserDefaults.h"
-#import "MASShortcut+Monitoring.h"
-
-@interface MASShortcutUserDefaultsHotKey : NSObject
-
-@property (nonatomic, readonly) NSString *userDefaultsKey;
-@property (nonatomic, copy) void (^handler)();
-@property (nonatomic, weak) id monitor;
-
-- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler;
-
-@end
-
-#pragma mark -
-
-@implementation MASShortcut (UserDefaults)
-
-+ (NSMutableDictionary *)registeredUserDefaultsHotKeys
-{
- static NSMutableDictionary *shared = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- shared = [NSMutableDictionary dictionary];
- });
- return shared;
-}
-
-+ (void)registerGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
-{
- MASShortcutUserDefaultsHotKey *hotKey = [[MASShortcutUserDefaultsHotKey alloc] initWithUserDefaultsKey:userDefaultsKey handler:handler];
- [[self registeredUserDefaultsHotKeys] setObject:hotKey forKey:userDefaultsKey];
-}
-
-+ (void)unregisterGlobalShortcutWithUserDefaultsKey:(NSString *)userDefaultsKey
-{
- NSMutableDictionary *registeredHotKeys = [self registeredUserDefaultsHotKeys];
- [registeredHotKeys removeObjectForKey:userDefaultsKey];
-}
-
-+ (void)setGlobalShortcut:(MASShortcut *)shortcut forUserDefaultsKey:(NSString *)userDefaultsKey
-{
- NSData *shortcutData = shortcut.data;
- if (shortcutData)
- [[NSUserDefaults standardUserDefaults] setObject:shortcutData forKey:userDefaultsKey];
- else
- [[NSUserDefaults standardUserDefaults] removeObjectForKey:userDefaultsKey];
-}
-
-@end
-
-#pragma mark -
-
-@implementation MASShortcutUserDefaultsHotKey {
- NSString *_observableKeyPath;
-}
-
-void *MASShortcutUserDefaultsContext = &MASShortcutUserDefaultsContext;
-
-- (id)initWithUserDefaultsKey:(NSString *)userDefaultsKey handler:(void (^)())handler
-{
- self = [super init];
- if (self) {
- _userDefaultsKey = userDefaultsKey.copy;
- _handler = [handler copy];
- _observableKeyPath = [@"values." stringByAppendingString:_userDefaultsKey];
- [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath options:NSKeyValueObservingOptionInitial context:MASShortcutUserDefaultsContext];
- }
- return self;
-}
-
-- (void)dealloc
-{
- [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self forKeyPath:_observableKeyPath context:MASShortcutUserDefaultsContext];
- [MASShortcut removeGlobalHotkeyMonitor:self.monitor];
-}
-
-#pragma mark -
-
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
-{
- if (context == MASShortcutUserDefaultsContext) {
- [MASShortcut removeGlobalHotkeyMonitor:self.monitor];
- [self installHotKeyFromUserDefaults];
- }
- else {
- [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
- }
-}
-
-- (void)installHotKeyFromUserDefaults
-{
- NSData *data = [[NSUserDefaults standardUserDefaults] dataForKey:_userDefaultsKey];
- MASShortcut *shortcut = [MASShortcut shortcutWithData:data];
- if (shortcut == nil) return;
- self.monitor = [MASShortcut addGlobalHotkeyMonitorWithShortcut:shortcut handler:self.handler];
-}
-
-@end
diff --git a/MASShortcut.h b/MASShortcut.h
deleted file mode 100644
index c9081a2..0000000
--- a/MASShortcut.h
+++ /dev/null
@@ -1,60 +0,0 @@
-#import <Carbon/Carbon.h>
-#import <AppKit/AppKit.h>
-
-#define MASShortcutChar(char) [NSString stringWithFormat:@"%C", (unsigned short)(char)]
-#define MASShortcutClear(flags) (flags & (NSControlKeyMask | NSShiftKeyMask | NSAlternateKeyMask | NSCommandKeyMask))
-#define MASShortcutCarbonFlags(cocoaFlags) (\
- (cocoaFlags & NSCommandKeyMask ? cmdKey : 0) | \
- (cocoaFlags & NSAlternateKeyMask ? optionKey : 0) | \
- (cocoaFlags & NSControlKeyMask ? controlKey : 0) | \
- (cocoaFlags & NSShiftKeyMask ? shiftKey : 0))
-
-// These glyphs are missed in Carbon.h
-enum {
- kMASShortcutGlyphEject = 0x23CF,
- kMASShortcutGlyphClear = 0x2715,
- kMASShortcutGlyphDeleteLeft = 0x232B,
- kMASShortcutGlyphDeleteRight = 0x2326,
- kMASShortcutGlyphLeftArrow = 0x2190,
- kMASShortcutGlyphRightArrow = 0x2192,
- kMASShortcutGlyphUpArrow = 0x2191,
- kMASShortcutGlyphDownArrow = 0x2193,
- kMASShortcutGlyphEscape = 0x238B,
- kMASShortcutGlyphHelp = 0x003F,
- kMASShortcutGlyphPageDown = 0x21DF,
- kMASShortcutGlyphPageUp = 0x21DE,
- kMASShortcutGlyphTabRight = 0x21E5,
- kMASShortcutGlyphReturn = 0x2305,
- kMASShortcutGlyphReturnR2L = 0x21A9,
- kMASShortcutGlyphPadClear = 0x2327,
- kMASShortcutGlyphNorthwestArrow = 0x2196,
- kMASShortcutGlyphSoutheastArrow = 0x2198,
-} MASShortcutGlyph;
-
-@interface MASShortcut : NSObject <NSSecureCoding>
-
-@property (nonatomic) NSUInteger keyCode;
-@property (nonatomic) NSUInteger modifierFlags;
-@property (nonatomic, readonly) UInt32 carbonKeyCode;
-@property (nonatomic, readonly) UInt32 carbonFlags;
-@property (nonatomic, readonly) NSString *keyCodeString;
-@property (nonatomic, readonly) NSString *keyCodeStringForKeyEquivalent;
-@property (nonatomic, readonly) NSString *modifierFlagsString;
-@property (nonatomic, readonly) NSData *data;
-@property (nonatomic, readonly) BOOL shouldBypass;
-@property (nonatomic, readonly, getter = isValid) BOOL valid;
-
-- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
-
-+ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags;
-+ (MASShortcut *)shortcutWithEvent:(NSEvent *)anEvent;
-+ (MASShortcut *)shortcutWithData:(NSData *)aData;
-
-- (BOOL)isTakenError:(NSError **)error;
-
-// The following API enable hotkeys with the Option key as the only modifier
-// For example, Option-G will not generate © and Option-R will not paste ®
-+ (void)setAllowsAnyHotkeyWithOptionModifier:(BOOL)allow;
-+ (BOOL)allowsAnyHotkeyWithOptionModifier;
-
-@end
diff --git a/MASShortcut.m b/MASShortcut.m
deleted file mode 100644
index ccc2035..0000000
--- a/MASShortcut.m
+++ /dev/null
@@ -1,354 +0,0 @@
-#import "MASShortcut.h"
-
-NSString *const MASShortcutKeyCode = @"KeyCode";
-NSString *const MASShortcutModifierFlags = @"ModifierFlags";
-
-@implementation MASShortcut {
- NSUInteger _keyCode; // NSNotFound if empty
- NSUInteger _modifierFlags; // 0 if empty
-}
-
-@synthesize modifierFlags = _modifierFlags;
-@synthesize keyCode = _keyCode;
-
-#pragma mark -
-
-+ (BOOL)supportsSecureCoding
-{
- return YES;
-}
-
-- (void)encodeWithCoder:(NSCoder *)coder
-{
- [coder encodeInteger:(self.keyCode != NSNotFound ? (NSInteger)self.keyCode : - 1) forKey:MASShortcutKeyCode];
- [coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags];
-}
-
-- (id)initWithCoder:(NSCoder *)decoder
-{
- self = [super init];
- if (self) {
- NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode];
- self.keyCode = (code < 0 ? NSNotFound : (NSUInteger)code);
- self.modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags];
- }
- return self;
-}
-
-- (id)initWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
-{
- self = [super init];
- if (self) {
- _keyCode = code;
- _modifierFlags = MASShortcutClear(flags);
- }
- return self;
-}
-
-+ (MASShortcut *)shortcutWithKeyCode:(NSUInteger)code modifierFlags:(NSUInteger)flags
-{
- return [[self alloc] initWithKeyCode:code modifierFlags:flags];
-}
-
-+ (MASShortcut *)shortcutWithEvent:(NSEvent *)event
-{
- return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags];
-}
-
-+ (MASShortcut *)shortcutWithData:(NSData *)data
-{
- id shortcut = (data ? [NSKeyedUnarchiver unarchiveObjectWithData:data] : nil);
- return shortcut;
-}
-
-#pragma mark - Shortcut accessors
-
-- (NSData *)data
-{
- return [NSKeyedArchiver archivedDataWithRootObject:self];
-}
-
-- (void)setModifierFlags:(NSUInteger)value
-{
- _modifierFlags = MASShortcutClear(value);
-}
-
-- (UInt32)carbonKeyCode
-{
- return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode);
-}
-
-- (UInt32)carbonFlags
-{
- return MASShortcutCarbonFlags(self.modifierFlags);
-}
-
-- (NSString *)description
-{
- return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString];
-}
-
-- (NSString *)keyCodeStringForKeyEquivalent
-{
- NSString *keyCodeString = self.keyCodeString;
- if (keyCodeString.length > 1) {
- switch (self.keyCode) {
- case kVK_F1: return MASShortcutChar(0xF704);
- case kVK_F2: return MASShortcutChar(0xF705);
- case kVK_F3: return MASShortcutChar(0xF706);
- case kVK_F4: return MASShortcutChar(0xF707);
- case kVK_F5: return MASShortcutChar(0xF708);
- case kVK_F6: return MASShortcutChar(0xF709);
- case kVK_F7: return MASShortcutChar(0xF70a);
- case kVK_F8: return MASShortcutChar(0xF70b);
- case kVK_F9: return MASShortcutChar(0xF70c);
- case kVK_F10: return MASShortcutChar(0xF70d);
- case kVK_F11: return MASShortcutChar(0xF70e);
- case kVK_F12: return MASShortcutChar(0xF70f);
- // From this point down I am guessing F13 etc come sequentially, I don't have a keyboard to test.
- case kVK_F13: return MASShortcutChar(0xF710);
- case kVK_F14: return MASShortcutChar(0xF711);
- case kVK_F15: return MASShortcutChar(0xF712);
- case kVK_F16: return MASShortcutChar(0xF713);
- case kVK_F17: return MASShortcutChar(0xF714);
- case kVK_F18: return MASShortcutChar(0xF715);
- case kVK_F19: return MASShortcutChar(0xF716);
- case kVK_Space: return MASShortcutChar(0x20);
- default: return @"";
- }
- }
- return keyCodeString.lowercaseString;
-}
-
-- (NSString *)keyCodeString
-{
- // Some key codes don't have an equivalent
- switch (self.keyCode) {
- case NSNotFound: return @"";
- case kVK_F1: return @"F1";
- case kVK_F2: return @"F2";
- case kVK_F3: return @"F3";
- case kVK_F4: return @"F4";
- case kVK_F5: return @"F5";
- case kVK_F6: return @"F6";
- case kVK_F7: return @"F7";
- case kVK_F8: return @"F8";
- case kVK_F9: return @"F9";
- case kVK_F10: return @"F10";
- case kVK_F11: return @"F11";
- case kVK_F12: return @"F12";
- case kVK_F13: return @"F13";
- case kVK_F14: return @"F14";
- case kVK_F15: return @"F15";
- case kVK_F16: return @"F16";
- case kVK_F17: return @"F17";
- case kVK_F18: return @"F18";
- case kVK_F19: return @"F19";
- case kVK_Space: return NSLocalizedString(@"Space", @"Shortcut glyph name for SPACE key");
- case kVK_Escape: return MASShortcutChar(kMASShortcutGlyphEscape);
- case kVK_Delete: return MASShortcutChar(kMASShortcutGlyphDeleteLeft);
- case kVK_ForwardDelete: return MASShortcutChar(kMASShortcutGlyphDeleteRight);
- case kVK_LeftArrow: return MASShortcutChar(kMASShortcutGlyphLeftArrow);
- case kVK_RightArrow: return MASShortcutChar(kMASShortcutGlyphRightArrow);
- case kVK_UpArrow: return MASShortcutChar(kMASShortcutGlyphUpArrow);
- case kVK_DownArrow: return MASShortcutChar(kMASShortcutGlyphDownArrow);
- case kVK_Help: return MASShortcutChar(kMASShortcutGlyphHelp);
- case kVK_PageUp: return MASShortcutChar(kMASShortcutGlyphPageUp);
- case kVK_PageDown: return MASShortcutChar(kMASShortcutGlyphPageDown);
- case kVK_Tab: return MASShortcutChar(kMASShortcutGlyphTabRight);
- case kVK_Return: return MASShortcutChar(kMASShortcutGlyphReturnR2L);
-
- // Keypad
- case kVK_ANSI_Keypad0: return @"0";
- case kVK_ANSI_Keypad1: return @"1";
- case kVK_ANSI_Keypad2: return @"2";
- case kVK_ANSI_Keypad3: return @"3";
- case kVK_ANSI_Keypad4: return @"4";
- case kVK_ANSI_Keypad5: return @"5";
- case kVK_ANSI_Keypad6: return @"6";
- case kVK_ANSI_Keypad7: return @"7";
- case kVK_ANSI_Keypad8: return @"8";
- case kVK_ANSI_Keypad9: return @"9";
- case kVK_ANSI_KeypadDecimal: return @".";
- case kVK_ANSI_KeypadMultiply: return @"*";
- case kVK_ANSI_KeypadPlus: return @"+";
- case kVK_ANSI_KeypadClear: return MASShortcutChar(kMASShortcutGlyphPadClear);
- case kVK_ANSI_KeypadDivide: return @"/";
- case kVK_ANSI_KeypadEnter: return MASShortcutChar(kMASShortcutGlyphReturn);
- case kVK_ANSI_KeypadMinus: return @"–";
- case kVK_ANSI_KeypadEquals: return @"=";
-
- // Hardcode
- case 119: return MASShortcutChar(kMASShortcutGlyphSoutheastArrow);
- case 115: return MASShortcutChar(kMASShortcutGlyphNorthwestArrow);
- }
-
- // Everything else should be printable so look it up in the current keyboard
- OSStatus error = noErr;
- NSString *keystroke = nil;
- TISInputSourceRef inputSource = TISCopyCurrentKeyboardLayoutInputSource();
- if (inputSource) {
- CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData);
- if (layoutDataRef) {
- UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef);
- UniCharCount length = 0;
- UniChar chars[256] = { 0 };
- UInt32 deadKeyState = 0;
- error = UCKeyTranslate(layoutData, (UInt16)self.keyCode, kUCKeyActionDisplay, 0, // No modifiers
- LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState,
- sizeof(chars) / sizeof(UniChar), &length, chars);
- keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @"");
- }
- CFRelease(inputSource);
- }
-
- // Validate keystroke
- if (keystroke.length) {
- static NSMutableCharacterSet *validChars = nil;
- if (validChars == nil) {
- validChars = [[NSMutableCharacterSet alloc] init];
- [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]];
- [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
- [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]];
- }
- for (NSUInteger i = 0, length = keystroke.length; i < length; i++) {
- if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) {
- keystroke = @"";
- break;
- }
- }
- }
-
- // Finally, we've got a shortcut!
- return keystroke.uppercaseString;
-}
-
-- (NSString *)modifierFlagsString
-{
- unichar chars[4];
- NSUInteger count = 0;
- // These are in the same order as the menu manager shows them
- if (self.modifierFlags & NSControlKeyMask) chars[count++] = kControlUnicode;
- if (self.modifierFlags & NSAlternateKeyMask) chars[count++] = kOptionUnicode;
- if (self.modifierFlags & NSShiftKeyMask) chars[count++] = kShiftUnicode;
- if (self.modifierFlags & NSCommandKeyMask) chars[count++] = kCommandUnicode;
- return (count ? [NSString stringWithCharacters:chars length:count] : @"");
-}
-
-#pragma mark - Validation logic
-
-- (BOOL)shouldBypass
-{
- NSString *codeString = self.keyCodeString;
- return (self.modifierFlags == NSCommandKeyMask) && ([codeString isEqualToString:@"W"] || [codeString isEqualToString:@"Q"]);
-}
-
-BOOL MASShortcutAllowsAnyHotkeyWithOptionModifier = NO;
-
-+ (void)setAllowsAnyHotkeyWithOptionModifier:(BOOL)allow
-{
- MASShortcutAllowsAnyHotkeyWithOptionModifier = allow;
-}
-
-+ (BOOL)allowsAnyHotkeyWithOptionModifier
-{
- return MASShortcutAllowsAnyHotkeyWithOptionModifier;
-}
-
-- (BOOL)isValid
-{
- // Allow any function key with any combination of modifiers
- BOOL includesFunctionKey = ((_keyCode == kVK_F1) || (_keyCode == kVK_F2) || (_keyCode == kVK_F3) || (_keyCode == kVK_F4) ||
- (_keyCode == kVK_F5) || (_keyCode == kVK_F6) || (_keyCode == kVK_F7) || (_keyCode == kVK_F8) ||
- (_keyCode == kVK_F9) || (_keyCode == kVK_F10) || (_keyCode == kVK_F11) || (_keyCode == kVK_F12) ||
- (_keyCode == kVK_F13) || (_keyCode == kVK_F14) || (_keyCode == kVK_F15) || (_keyCode == kVK_F16) ||
- (_keyCode == kVK_F17) || (_keyCode == kVK_F18) || (_keyCode == kVK_F19) || (_keyCode == kVK_F20));
- if (includesFunctionKey) return YES;
-
- // Do not allow any other key without modifiers
- BOOL hasModifierFlags = (_modifierFlags > 0);
- if (!hasModifierFlags) return NO;
-
- // Allow any hotkey containing Control or Command modifier
- BOOL includesCommand = ((_modifierFlags & NSCommandKeyMask) > 0);
- BOOL includesControl = ((_modifierFlags & NSControlKeyMask) > 0);
- if (includesCommand || includesControl) return YES;
-
- // Allow Option key only in selected cases
- BOOL includesOption = ((_modifierFlags & NSAlternateKeyMask) > 0);
- if (includesOption) {
-
- // Always allow Option-Space and Option-Escape because they do not have any bind system commands
- if ((_keyCode == kVK_Space) || (_keyCode == kVK_Escape)) return YES;
-
- // Allow Option modifier with any key even if it will break the system binding
- if ([[self class] allowsAnyHotkeyWithOptionModifier]) return YES;
- }
-
- // The hotkey does not have any modifiers or violates system bindings
- return NO;
-}
-
-- (BOOL)isKeyEquivalent:(NSString *)keyEquivalent flags:(NSUInteger)flags takenInMenu:(NSMenu *)menu error:(NSError **)outError
-{
- for (NSMenuItem *menuItem in menu.itemArray) {
- if (menuItem.hasSubmenu && [self isKeyEquivalent:keyEquivalent flags:flags takenInMenu:menuItem.submenu error:outError]) return YES;
-
- BOOL equalFlags = (MASShortcutClear(menuItem.keyEquivalentModifierMask) == flags);
- BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent];
-
- // Check if the cases are different, we know ours is lower and that shift is included in our modifiers
- // If theirs is capitol, we need to add shift to their modifiers
- if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) {
- equalFlags = (MASShortcutClear(menuItem.keyEquivalentModifierMask | NSShiftKeyMask) == flags);
- }
-
- if (equalFlags && equalHotkeyLowercase) {
- if (outError) {
- NSString *format = NSLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.",
- @"Message for alert when shortcut is already used");
- NSDictionary *info = [NSDictionary dictionaryWithObject:[NSString stringWithFormat:format, menuItem.title]
- forKey:NSLocalizedDescriptionKey];
- *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
- }
- return YES;
- }
- }
- return NO;
-}
-
-- (BOOL)isTakenError:(NSError **)outError
-{
- CFArrayRef globalHotKeys;
- BOOL isTaken = NO;
- if (CopySymbolicHotKeys(&globalHotKeys) == noErr) {
-
- // Enumerate all global hotkeys and check if any of them matches current shortcut
- for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) {
- CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i);
- CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode);
- CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers);
- CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled);
-
- if (([(__bridge NSNumber *)code unsignedIntegerValue] == self.keyCode) &&
- ([(__bridge NSNumber *)flags unsignedIntegerValue] == self.carbonFlags) &&
- ([(__bridge NSNumber *)enabled boolValue])) {
-
- if (outError) {
- NSString *description = NSLocalizedString(@"This combination cannot be used because it is already used by a system-wide "
- @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts "
- @"can be changed in the Keyboard & Mouse panel in System Preferences.",
- @"Message for alert when shortcut is already used by the system");
- NSDictionary *info = [NSDictionary dictionaryWithObject:description forKey:NSLocalizedDescriptionKey];
- *outError = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:info];
- }
- isTaken = YES;
- break;
- }
- }
- CFRelease(globalHotKeys);
- }
- return (isTaken || [self isKeyEquivalent:self.keyCodeStringForKeyEquivalent flags:self.modifierFlags takenInMenu:[NSApp mainMenu] error:outError]);
-}
-
-@end
diff --git a/MASShortcut.podspec b/MASShortcut.podspec
index f45b87e..fe3b2a6 100644
--- a/MASShortcut.podspec
+++ b/MASShortcut.podspec
@@ -1,14 +1,16 @@
Pod::Spec.new do |s|
- s.platform = :osx
- s.osx.deployment_target = "10.7"
s.name = 'MASShortcut'
- s.version = '1.3.1'
+ s.version = '2.0.0'
s.summary = 'Modern framework for managing global keyboard shortcuts compatible with Mac App Store'
s.homepage = 'https://github.com/shpakovski/MASShortcut'
s.authors = { 'Vadim Shpakovski' => 'vadim@shpakovski.com' }
s.license = 'BSD 2-clause'
- s.source = { :git => 'https://github.com/shpakovski/MASShortcut.git', :tag => '1.3.1' }
- s.source_files = '*.{h,m}'
+ s.platform = :osx
+ s.osx.deployment_target = "10.7"
+
+ s.source = { :git => 'https://github.com/shpakovski/MASShortcut.git', :tag => '2.0.0' }
+ s.source_files = 'Framework/*.{h,m}'
+ s.exclude_files = 'Framework/*Tests.m'
s.osx.frameworks = 'Carbon', 'AppKit'
s.requires_arc = true
end
diff --git a/MASShortcut.xcodeproj/project.pbxproj b/MASShortcut.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..4b35e8b
--- /dev/null
+++ b/MASShortcut.xcodeproj/project.pbxproj
@@ -0,0 +1,652 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0D827CD71990D4420010B8EF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD61990D4420010B8EF /* Cocoa.framework */; };
+ 0D827D251990D55E0010B8EF /* MASShortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D1B1990D55E0010B8EF /* MASShortcut.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0D827D261990D55E0010B8EF /* MASShortcut.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D1C1990D55E0010B8EF /* MASShortcut.m */; };
+ 0D827D2B1990D55E0010B8EF /* MASShortcutView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D211990D55E0010B8EF /* MASShortcutView.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D221990D55E0010B8EF /* MASShortcutView.m */; };
+ 0D827D381990D5E70010B8EF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD61990D4420010B8EF /* Cocoa.framework */; };
+ 0D827D6F1990D6110010B8EF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D6A1990D6110010B8EF /* AppDelegate.m */; };
+ 0D827D711990D6110010B8EF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D6D1990D6110010B8EF /* main.m */; };
+ 0D827D721990D6110010B8EF /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0D827D6E1990D6110010B8EF /* MainMenu.xib */; };
+ 0D827D731990D6590010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; };
+ 0D827D751990D6A60010B8EF /* MASShortcut.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; };
+ 0D827D771990F81E0010B8EF /* Shortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D761990F81E0010B8EF /* Shortcut.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0D827D8419910AFF0010B8EF /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CEB1990D4420010B8EF /* XCTest.framework */; };
+ 0D827D9419910B740010B8EF /* MASShortcutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9319910B740010B8EF /* MASShortcutTests.m */; };
+ 0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; };
+ 0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9619910FF70010B8EF /* MASKeyCodes.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0D827D99199110F60010B8EF /* Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D98199110F60010B8EF /* Prefix.pch */; };
+ 0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */; };
+ 0D827DA519912D240010B8EF /* MASShortcutMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0D827DAD199132840010B8EF /* MASShortcutBinder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827DAB199132840010B8EF /* MASShortcutBinder.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0DC2F17619922798003A0131 /* MASHotKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F17419922798003A0131 /* MASHotKey.h */; };
+ 0DC2F17719922798003A0131 /* MASHotKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F17519922798003A0131 /* MASHotKey.m */; };
+ 0DC2F17C199232EA003A0131 /* MASShortcutMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */; };
+ 0DC2F17D199232F7003A0131 /* MASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827DAC199132840010B8EF /* MASShortcutBinder.m */; };
+ 0DC2F18919925F8F003A0131 /* MASShortcutBinderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */; };
+ 0DC2F18D1993708A003A0131 /* MASDictionaryTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0DC2F18E1993708A003A0131 /* MASDictionaryTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */; };
+ 0DC2F190199372B4003A0131 /* MASDictionaryTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */; };
+ 0DC2F19819938EFA003A0131 /* MASShortcutView+Bindings.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */; settings = {ATTRIBUTES = (Public, ); }; };
+ 0DC2F19919938EFA003A0131 /* MASShortcutView+Bindings.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 0D827D8E19910AFF0010B8EF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 0D827CCA1990D4420010B8EF /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 0D827CD21990D4420010B8EF;
+ remoteInfo = MASShortcut;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 0D827D741990D6980010B8EF /* Copy Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 0D827D751990D6A60010B8EF /* MASShortcut.framework in Copy Frameworks */,
+ );
+ name = "Copy Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0D827CD31990D4420010B8EF /* MASShortcut.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MASShortcut.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0D827CD61990D4420010B8EF /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; };
+ 0D827CD91990D4420010B8EF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
+ 0D827CDA1990D4420010B8EF /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; };
+ 0D827CDB1990D4420010B8EF /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
+ 0D827CEB1990D4420010B8EF /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
+ 0D827D1B1990D55E0010B8EF /* MASShortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcut.h; path = Framework/MASShortcut.h; sourceTree = "<group>"; };
+ 0D827D1C1990D55E0010B8EF /* MASShortcut.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcut.m; path = Framework/MASShortcut.m; sourceTree = "<group>"; };
+ 0D827D211990D55E0010B8EF /* MASShortcutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutView.h; path = Framework/MASShortcutView.h; sourceTree = "<group>"; };
+ 0D827D221990D55E0010B8EF /* MASShortcutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutView.m; path = Framework/MASShortcutView.m; sourceTree = "<group>"; };
+ 0D827D2F1990D5640010B8EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Framework/Info.plist; sourceTree = "<group>"; };
+ 0D827D371990D5E70010B8EF /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0D827D691990D6110010B8EF /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+ 0D827D6A1990D6110010B8EF /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+ 0D827D6B1990D6110010B8EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 0D827D6C1990D6110010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = "<group>"; };
+ 0D827D6D1990D6110010B8EF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+ 0D827D6E1990D6110010B8EF /* MainMenu.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
+ 0D827D761990F81E0010B8EF /* Shortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Shortcut.h; path = Framework/Shortcut.h; sourceTree = "<group>"; };
+ 0D827D8319910AFF0010B8EF /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 0D827D8719910AFF0010B8EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+ 0D827D8D19910AFF0010B8EF /* Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = "<group>"; };
+ 0D827D9319910B740010B8EF /* MASShortcutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutTests.m; path = Framework/MASShortcutTests.m; sourceTree = "<group>"; };
+ 0D827D9619910FF70010B8EF /* MASKeyCodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASKeyCodes.h; path = Framework/MASKeyCodes.h; sourceTree = "<group>"; };
+ 0D827D98199110F60010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Prefix.pch; path = Framework/Prefix.pch; sourceTree = "<group>"; };
+ 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutValidator.h; path = Framework/MASShortcutValidator.h; sourceTree = "<group>"; };
+ 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutValidator.m; path = Framework/MASShortcutValidator.m; sourceTree = "<group>"; };
+ 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutMonitor.h; path = Framework/MASShortcutMonitor.h; sourceTree = "<group>"; };
+ 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutMonitor.m; path = Framework/MASShortcutMonitor.m; sourceTree = "<group>"; };
+ 0D827DAB199132840010B8EF /* MASShortcutBinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASShortcutBinder.h; path = Framework/MASShortcutBinder.h; sourceTree = "<group>"; };
+ 0D827DAC199132840010B8EF /* MASShortcutBinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutBinder.m; path = Framework/MASShortcutBinder.m; sourceTree = "<group>"; };
+ 0DC2F17419922798003A0131 /* MASHotKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASHotKey.h; path = Framework/MASHotKey.h; sourceTree = "<group>"; };
+ 0DC2F17519922798003A0131 /* MASHotKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASHotKey.m; path = Framework/MASHotKey.m; sourceTree = "<group>"; };
+ 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASShortcutBinderTests.m; path = Framework/MASShortcutBinderTests.m; sourceTree = "<group>"; };
+ 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MASDictionaryTransformer.h; path = Framework/MASDictionaryTransformer.h; sourceTree = "<group>"; };
+ 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASDictionaryTransformer.m; path = Framework/MASDictionaryTransformer.m; sourceTree = "<group>"; };
+ 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MASDictionaryTransformerTests.m; path = Framework/MASDictionaryTransformerTests.m; sourceTree = "<group>"; };
+ 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "MASShortcutView+Bindings.h"; path = "Framework/MASShortcutView+Bindings.h"; sourceTree = "<group>"; };
+ 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "MASShortcutView+Bindings.m"; path = "Framework/MASShortcutView+Bindings.m"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 0D827CCF1990D4420010B8EF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D827CD71990D4420010B8EF /* Cocoa.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0D827D341990D5E70010B8EF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D827D381990D5E70010B8EF /* Cocoa.framework in Frameworks */,
+ 0D827D731990D6590010B8EF /* MASShortcut.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0D827D8019910AFF0010B8EF /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D827D8419910AFF0010B8EF /* XCTest.framework in Frameworks */,
+ 0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 0D827CC91990D4420010B8EF = {
+ isa = PBXGroup;
+ children = (
+ 0D827D151990D4D70010B8EF /* Framework */,
+ 0D827D8519910AFF0010B8EF /* Test Support */,
+ 0D827D681990D6110010B8EF /* Demo */,
+ 0D827CD51990D4420010B8EF /* Frameworks */,
+ 0D827CD41990D4420010B8EF /* Products */,
+ );
+ sourceTree = "<group>";
+ };
+ 0D827CD41990D4420010B8EF /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827CD31990D4420010B8EF /* MASShortcut.framework */,
+ 0D827D371990D5E70010B8EF /* Demo.app */,
+ 0D827D8319910AFF0010B8EF /* Tests.xctest */,
+ );
+ name = Products;
+ sourceTree = "<group>";
+ };
+ 0D827CD51990D4420010B8EF /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827CD61990D4420010B8EF /* Cocoa.framework */,
+ 0D827CEB1990D4420010B8EF /* XCTest.framework */,
+ 0D827CD91990D4420010B8EF /* Foundation.framework */,
+ 0D827CDA1990D4420010B8EF /* CoreData.framework */,
+ 0D827CDB1990D4420010B8EF /* AppKit.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "<group>";
+ };
+ 0D827D151990D4D70010B8EF /* Framework */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827DA019912A660010B8EF /* Model */,
+ 0D827DA219912A870010B8EF /* Monitoring */,
+ 0DC2F18A19937060003A0131 /* User Defaults Storage */,
+ 0D827DA119912A6D0010B8EF /* UI */,
+ 0D827D2F1990D5640010B8EF /* Info.plist */,
+ 0D827D98199110F60010B8EF /* Prefix.pch */,
+ 0D827D761990F81E0010B8EF /* Shortcut.h */,
+ );
+ name = Framework;
+ sourceTree = "<group>";
+ };
+ 0D827D681990D6110010B8EF /* Demo */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827D691990D6110010B8EF /* AppDelegate.h */,
+ 0D827D6A1990D6110010B8EF /* AppDelegate.m */,
+ 0D827D6E1990D6110010B8EF /* MainMenu.xib */,
+ 0D827D6B1990D6110010B8EF /* Info.plist */,
+ 0D827D6C1990D6110010B8EF /* Prefix.pch */,
+ 0D827D6D1990D6110010B8EF /* main.m */,
+ );
+ path = Demo;
+ sourceTree = "<group>";
+ };
+ 0D827D8519910AFF0010B8EF /* Test Support */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827D8719910AFF0010B8EF /* Info.plist */,
+ 0D827D8D19910AFF0010B8EF /* Prefix.pch */,
+ );
+ name = "Test Support";
+ path = Tests;
+ sourceTree = "<group>";
+ };
+ 0D827DA019912A660010B8EF /* Model */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827D9619910FF70010B8EF /* MASKeyCodes.h */,
+ 0D827D1B1990D55E0010B8EF /* MASShortcut.h */,
+ 0D827D1C1990D55E0010B8EF /* MASShortcut.m */,
+ 0D827D9319910B740010B8EF /* MASShortcutTests.m */,
+ 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */,
+ 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */,
+ );
+ name = Model;
+ sourceTree = "<group>";
+ };
+ 0D827DA119912A6D0010B8EF /* UI */ = {
+ isa = PBXGroup;
+ children = (
+ 0D827D211990D55E0010B8EF /* MASShortcutView.h */,
+ 0D827D221990D55E0010B8EF /* MASShortcutView.m */,
+ 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */,
+ 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */,
+ );
+ name = UI;
+ sourceTree = "<group>";
+ };
+ 0D827DA219912A870010B8EF /* Monitoring */ = {
+ isa = PBXGroup;
+ children = (
+ 0DC2F17419922798003A0131 /* MASHotKey.h */,
+ 0DC2F17519922798003A0131 /* MASHotKey.m */,
+ 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */,
+ 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */,
+ );
+ name = Monitoring;
+ sourceTree = "<group>";
+ };
+ 0DC2F18A19937060003A0131 /* User Defaults Storage */ = {
+ isa = PBXGroup;
+ children = (
+ 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */,
+ 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */,
+ 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */,
+ 0D827DAB199132840010B8EF /* MASShortcutBinder.h */,
+ 0D827DAC199132840010B8EF /* MASShortcutBinder.m */,
+ 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */,
+ );
+ name = "User Defaults Storage";
+ sourceTree = "<group>";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXHeadersBuildPhase section */
+ 0D827CD01990D4420010B8EF /* Headers */ = {
+ isa = PBXHeadersBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */,
+ 0D827D2B1990D55E0010B8EF /* MASShortcutView.h in Headers */,
+ 0D827D99199110F60010B8EF /* Prefix.pch in Headers */,
+ 0D827D251990D55E0010B8EF /* MASShortcut.h in Headers */,
+ 0DC2F19819938EFA003A0131 /* MASShortcutView+Bindings.h in Headers */,
+ 0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */,
+ 0DC2F18D1993708A003A0131 /* MASDictionaryTransformer.h in Headers */,
+ 0D827DA519912D240010B8EF /* MASShortcutMonitor.h in Headers */,
+ 0D827DAD199132840010B8EF /* MASShortcutBinder.h in Headers */,
+ 0DC2F17619922798003A0131 /* MASHotKey.h in Headers */,
+ 0D827D771990F81E0010B8EF /* Shortcut.h in Headers */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+ 0D827CD21990D4420010B8EF /* MASShortcut */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0D827CFB1990D4420010B8EF /* Build configuration list for PBXNativeTarget "MASShortcut" */;
+ buildPhases = (
+ 0D827CCE1990D4420010B8EF /* Sources */,
+ 0D827CCF1990D4420010B8EF /* Frameworks */,
+ 0D827CD01990D4420010B8EF /* Headers */,
+ 0D827CD11990D4420010B8EF /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = MASShortcut;
+ productName = MASShortcut;
+ productReference = 0D827CD31990D4420010B8EF /* MASShortcut.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+ 0D827D361990D5E70010B8EF /* Demo */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0D827D661990D5E70010B8EF /* Build configuration list for PBXNativeTarget "Demo" */;
+ buildPhases = (
+ 0D827D331990D5E70010B8EF /* Sources */,
+ 0D827D341990D5E70010B8EF /* Frameworks */,
+ 0D827D351990D5E70010B8EF /* Resources */,
+ 0D827D741990D6980010B8EF /* Copy Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Demo;
+ productName = Demo;
+ productReference = 0D827D371990D5E70010B8EF /* Demo.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 0D827D8219910AFF0010B8EF /* Tests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "Tests" */;
+ buildPhases = (
+ 0D827D7F19910AFF0010B8EF /* Sources */,
+ 0D827D8019910AFF0010B8EF /* Frameworks */,
+ 0D827D8119910AFF0010B8EF /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 0D827D8F19910AFF0010B8EF /* PBXTargetDependency */,
+ );
+ name = Tests;
+ productName = Tests;
+ productReference = 0D827D8319910AFF0010B8EF /* Tests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 0D827CCA1990D4420010B8EF /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 0510;
+ ORGANIZATIONNAME = "Vadim Shpakovski";
+ TargetAttributes = {
+ 0D827D8219910AFF0010B8EF = {
+ TestTargetID = 0D827CD21990D4420010B8EF;
+ };
+ };
+ };
+ buildConfigurationList = 0D827CCD1990D4420010B8EF /* Build configuration list for PBXProject "MASShortcut" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = English;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 0D827CC91990D4420010B8EF;
+ productRefGroup = 0D827CD41990D4420010B8EF /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 0D827CD21990D4420010B8EF /* MASShortcut */,
+ 0D827D361990D5E70010B8EF /* Demo */,
+ 0D827D8219910AFF0010B8EF /* Tests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 0D827CD11990D4420010B8EF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0D827D351990D5E70010B8EF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D827D721990D6110010B8EF /* MainMenu.xib in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0D827D8119910AFF0010B8EF /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 0D827CCE1990D4420010B8EF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0DC2F17719922798003A0131 /* MASHotKey.m in Sources */,
+ 0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */,
+ 0DC2F17C199232EA003A0131 /* MASShortcutMonitor.m in Sources */,
+ 0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */,
+ 0D827D261990D55E0010B8EF /* MASShortcut.m in Sources */,
+ 0DC2F18E1993708A003A0131 /* MASDictionaryTransformer.m in Sources */,
+ 0DC2F19919938EFA003A0131 /* MASShortcutView+Bindings.m in Sources */,
+ 0DC2F17D199232F7003A0131 /* MASShortcutBinder.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0D827D331990D5E70010B8EF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0D827D711990D6110010B8EF /* main.m in Sources */,
+ 0D827D6F1990D6110010B8EF /* AppDelegate.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 0D827D7F19910AFF0010B8EF /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0DC2F190199372B4003A0131 /* MASDictionaryTransformerTests.m in Sources */,
+ 0D827D9419910B740010B8EF /* MASShortcutTests.m in Sources */,
+ 0DC2F18919925F8F003A0131 /* MASShortcutBinderTests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 0D827D8F19910AFF0010B8EF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 0D827CD21990D4420010B8EF /* MASShortcut */;
+ targetProxy = 0D827D8E19910AFF0010B8EF /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 0D827CF91990D4420010B8EF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_SYMBOLS_PRIVATE_EXTERN = NO;
+ GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.9;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = macosx;
+ };
+ name = Debug;
+ };
+ 0D827CFA1990D4420010B8EF /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = YES;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_ENABLE_OBJC_EXCEPTIONS = YES;
+ GCC_TREAT_WARNINGS_AS_ERRORS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ MACOSX_DEPLOYMENT_TARGET = 10.9;
+ SDKROOT = macosx;
+ };
+ name = Release;
+ };
+ 0D827CFC1990D4420010B8EF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ COMBINE_HIDPI_IMAGES = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ FRAMEWORK_VERSION = A;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = Framework/Prefix.pch;
+ INFOPLIST_FILE = Framework/Info.plist;
+ INSTALL_PATH = "@executable_path/../Frameworks";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ WRAPPER_EXTENSION = framework;
+ };
+ name = Debug;
+ };
+ 0D827CFD1990D4420010B8EF /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ COMBINE_HIDPI_IMAGES = YES;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ FRAMEWORK_VERSION = A;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = Framework/Prefix.pch;
+ INFOPLIST_FILE = Framework/Info.plist;
+ INSTALL_PATH = "@executable_path/../Frameworks";
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SKIP_INSTALL = YES;
+ WRAPPER_EXTENSION = framework;
+ };
+ name = Release;
+ };
+ 0D827D621990D5E70010B8EF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ COMBINE_HIDPI_IMAGES = YES;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = Demo/Prefix.pch;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = Demo/Info.plist;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Debug;
+ };
+ 0D827D631990D5E70010B8EF /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ COMBINE_HIDPI_IMAGES = YES;
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = Demo/Prefix.pch;
+ INFOPLIST_FILE = Demo/Info.plist;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = app;
+ };
+ name = Release;
+ };
+ 0D827D9019910AFF0010B8EF /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ "$(inherited)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = Tests/Prefix.pch;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = Tests/Info.plist;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Debug;
+ };
+ 0D827D9119910AFF0010B8EF /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(DEVELOPER_FRAMEWORKS_DIR)",
+ "$(inherited)",
+ );
+ GCC_PRECOMPILE_PREFIX_HEADER = YES;
+ GCC_PREFIX_HEADER = Tests/Prefix.pch;
+ INFOPLIST_FILE = Tests/Info.plist;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ WRAPPER_EXTENSION = xctest;
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 0D827CCD1990D4420010B8EF /* Build configuration list for PBXProject "MASShortcut" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D827CF91990D4420010B8EF /* Debug */,
+ 0D827CFA1990D4420010B8EF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 0D827CFB1990D4420010B8EF /* Build configuration list for PBXNativeTarget "MASShortcut" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D827CFC1990D4420010B8EF /* Debug */,
+ 0D827CFD1990D4420010B8EF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 0D827D661990D5E70010B8EF /* Build configuration list for PBXNativeTarget "Demo" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D827D621990D5E70010B8EF /* Debug */,
+ 0D827D631990D5E70010B8EF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "Tests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D827D9019910AFF0010B8EF /* Debug */,
+ 0D827D9119910AFF0010B8EF /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 0D827CCA1990D4420010B8EF /* Project object */;
+}
diff --git a/MASShortcutView+UserDefaults.h b/MASShortcutView+UserDefaults.h
deleted file mode 100644
index 05d3c5b..0000000
--- a/MASShortcutView+UserDefaults.h
+++ /dev/null
@@ -1,7 +0,0 @@
-#import "MASShortcutView.h"
-
-@interface MASShortcutView (UserDefaults)
-
-@property (nonatomic, copy) NSString *associatedUserDefaultsKey;
-
-@end
diff --git a/MASShortcutView+UserDefaults.m b/MASShortcutView+UserDefaults.m
deleted file mode 100644
index a84f0c9..0000000
--- a/MASShortcutView+UserDefaults.m
+++ /dev/null
@@ -1,125 +0,0 @@
-#import "MASShortcutView+UserDefaults.h"
-#import "MASShortcut.h"
-#import <objc/runtime.h>
-
-@interface MASShortcutDefaultsObserver : NSObject
-
-@property (nonatomic, readonly) NSString *userDefaultsKey;
-@property (nonatomic, readonly, weak) MASShortcutView *shortcutView;
-
-- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey;
-
-@end
-
-#pragma mark -
-
-@implementation MASShortcutView (UserDefaults)
-
-void *MASAssociatedDefaultsObserver = &MASAssociatedDefaultsObserver;
-
-- (NSString *)associatedUserDefaultsKey
-{
- MASShortcutDefaultsObserver *defaultsObserver = objc_getAssociatedObject(self, MASAssociatedDefaultsObserver);
- return defaultsObserver.userDefaultsKey;
-}
-
-- (void)setAssociatedUserDefaultsKey:(NSString *)associatedUserDefaultsKey
-{
- // First, stop observing previous shortcut view
- objc_setAssociatedObject(self, MASAssociatedDefaultsObserver, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-
- if (associatedUserDefaultsKey.length == 0) return;
-
- // Next, start observing current shortcut view
- MASShortcutDefaultsObserver *defaultsObserver = [[MASShortcutDefaultsObserver alloc] initWithShortcutView:self userDefaultsKey:associatedUserDefaultsKey];
- objc_setAssociatedObject(self, MASAssociatedDefaultsObserver, defaultsObserver, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
-}
-
-@end
-
-#pragma mark -
-
-@implementation MASShortcutDefaultsObserver {
- MASShortcut *_originalShortcut;
- BOOL _internalPreferenceChange;
- BOOL _internalShortcutChange;
-}
-
-- (id)initWithShortcutView:(MASShortcutView *)shortcutView userDefaultsKey:(NSString *)userDefaultsKey
-{
- self = [super init];
- if (self) {
- _originalShortcut = shortcutView.shortcutValue;
- _shortcutView = shortcutView;
- _userDefaultsKey = userDefaultsKey.copy;
- [self startObservingShortcutView];
- }
- return self;
-}
-
-- (void)dealloc
-{
- // __weak _shortcutView is not yet deallocated because it refers MASShortcutDefaultsObserver
- [self stopObservingShortcutView];
-}
-
-#pragma mark -
-
-void *kShortcutValueObserver = &kShortcutValueObserver;
-
-- (void)startObservingShortcutView
-{
- // Read initial shortcut value from user preferences
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
- NSData *data = [defaults dataForKey:_userDefaultsKey];
- _shortcutView.shortcutValue = [MASShortcut shortcutWithData:data];
-
- // Observe user preferences to update shortcut value when it changed
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(userDefaultsDidChange:) name:NSUserDefaultsDidChangeNotification object:defaults];
-
- // Observe the keyboard shortcut that user inputs by hand
- [_shortcutView addObserver:self forKeyPath:@"shortcutValue" options:0 context:kShortcutValueObserver];
-}
-
-- (void)userDefaultsDidChange:(NSNotification *)note
-{
- // Ignore notifications posted from -[self observeValueForKeyPath:]
- if (_internalPreferenceChange) return;
-
- _internalShortcutChange = YES;
- NSData *data = [note.object dataForKey:_userDefaultsKey];
- _shortcutView.shortcutValue = [MASShortcut shortcutWithData:data];
- _internalShortcutChange = NO;
-}
-
-- (void)stopObservingShortcutView
-{
- // Stop observing keyboard hotkeys entered by user in the shortcut view
- [_shortcutView removeObserver:self forKeyPath:@"shortcutValue" context:kShortcutValueObserver];
-
- // Stop observing user preferences
- [[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification object:[NSUserDefaults standardUserDefaults]];
-
- // Restore original hotkey in the shortcut view
- _shortcutView.shortcutValue = _originalShortcut;
-}
-
-- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
-{
- if (context == kShortcutValueObserver) {
- if (_internalShortcutChange) return;
- MASShortcut *shortcut = [object valueForKey:keyPath];
- _internalPreferenceChange = YES;
-
- NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
- [defaults setObject:(shortcut.data ?: [NSKeyedArchiver archivedDataWithRootObject:nil]) forKey:_userDefaultsKey];
- [defaults synchronize];
-
- _internalPreferenceChange = NO;
- }
- else {
- [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
- }
-}
-
-@end
diff --git a/MASShortcutView.h b/MASShortcutView.h
deleted file mode 100644
index c8a46bb..0000000
--- a/MASShortcutView.h
+++ /dev/null
@@ -1,23 +0,0 @@
-#import <AppKit/AppKit.h>
-
-@class MASShortcut;
-
-typedef enum {
- MASShortcutViewAppearanceDefault = 0, // Height = 19 px
- MASShortcutViewAppearanceTexturedRect, // Height = 25 px
- MASShortcutViewAppearanceRounded, // Height = 43 px
- MASShortcutViewAppearanceFlat
-} MASShortcutViewAppearance;
-
-@interface MASShortcutView : NSView
-
-@property (nonatomic, strong) MASShortcut *shortcutValue;
-@property (nonatomic, getter = isRecording) BOOL recording;
-@property (nonatomic, getter = isEnabled) BOOL enabled;
-@property (nonatomic, copy) void (^shortcutValueChange)(MASShortcutView *sender);
-@property (nonatomic) MASShortcutViewAppearance appearance;
-
-/// Returns custom class for drawing control.
-+ (Class)shortcutCellClass;
-
-@end
diff --git a/README.md b/README.md
index bbdf2c2..a4ea509 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,37 @@
# Intro
-Some time ago Cocoa developers used a brilliant framework [ShortcutRecorder](http://wafflesoftware.net/shortcut/) for managing keyboard shortcuts in application preferences. However, it became incompatible with a new plugin architecture of Xcode 4.
+Some time ago Cocoa developers used a brilliant framework [ShortcutRecorder](http://wafflesoftware.net/shortcut/) for managing keyboard shortcuts in application preferences. However, it became incompatible with the new plugin architecture of Xcode 4.
-The project MASShortcut introduces modern API and user interface for recording, storing and using global keyboard shortcuts. All code is compatible with Xcode 4.3, Mac OS X 10.7 and the sandboxed environment.
+The MASShortcut project introduces a modern API and user interface for recording, storing and using system-wide keyboard shortcuts. All code is compatible with recent Xcode & OS X versions and the sandboxed environment.
# Usage
I hope, it is really easy:
```objective-c
-// Drop a custom view into XIB, set its class to MASShortcutView and its height to 19. If you select another appearance style look up the correct values in MASShortcutView.h
+// Drop a custom view into XIB, set its class to MASShortcutView
+// and its height to 19. If you select another appearance style,
+// look up the correct height values in MASShortcutView.h.
@property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView;
-
-// Think up a preference key to store a global shortcut between launches
-NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
-// Assign the preference key and the shortcut view will take care of persistence
+// Pick a preference key to store the shortcut between launches
+static NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut";
+
+// Associate the shortcut view with user defaults
self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut;
-// Execute your block of code automatically when user triggers a shortcut from preferences
-[MASShortcut registerGlobalShortcutWithUserDefaultsKey:kPreferenceGlobalShortcut handler:^{
- // Let me know if you find a better or more convenient API.
+// Associate the preference key with an action
+[[MASShortcutBinder sharedBinder]
+ bindShortcutWithDefaultsKey:kPreferenceGlobalShortcut
+ toAction:^{
+ // Let me know if you find a better or a more convenient API.
}];
```
-To set an example, I made a demo project: [MASShortcutDemo](https://github.com/shpakovski/MASShortcutDemo). Enjoy!
+You can see a real usage example in the Demo target. Enjoy!
+
+# Notifications
-#Notifications
By registering for KVO notifications from `NSUserDefaultsController`, you can get a callback whenever a user changes the shortcut, allowing you to perform any UI updates, or other code handling tasks.
This is just as easy to implement:
diff --git a/Spec.md b/Spec.md
new file mode 100644
index 0000000..9a8663a
--- /dev/null
+++ b/Spec.md
@@ -0,0 +1,15 @@
+This is an attempt to specify some of the parts of the library so that it’s easier to spot bugs and regressions.
+
+The specification is expected to grow incrementally, as the developers update various parts of the code. If you hack on a part of the library that would benefit from a precise specification and is not documented here yet, please consider adding to the specification.
+
+Please stay high-level when writing the spec, do not document particular classes or other implementation details. The spec should be usable as a testing scenario – you should be able to walk through the spec and verify correct code behaviour on the library demo app.
+
+# Recording Shortcuts
+
+* If a shortcut has no modifiers and is not a function key (F1–F20), it must be rejected. (Examples: `A`, Shift-A.)
+* If the shortcut is plain Esc without modifiers, it must be rejected and cancels the recording.
+* If the shortcut is plain Backspace or plain Delete, it must be rejected, clears the recorded shortcut and cancels the recording.
+* If the shortcut is Cmd-W or Cmd-Q, the recording must be cancelled and the keypress passed through to the system, closing the window or quitting the app.
+* If a shortcut is already taken by system and is enabled, it must be rejected. (Examples: Cmd-S, Cmd-N. TBD: What exactly does it mean that the shortcut is “enabled”?)
+* TBD: Option-key handling.
+* All other shortcuts must be accepted. (Examples: Ctrl-Esc, Cmd-Delete, F16.) \ No newline at end of file
diff --git a/Tests/Info.plist b/Tests/Info.plist
new file mode 100644
index 0000000..0f3f601
--- /dev/null
+++ b/Tests/Info.plist
@@ -0,0 +1,22 @@
+<?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>CFBundleDevelopmentRegion</key>
+ <string>en</string>
+ <key>CFBundleExecutable</key>
+ <string>${EXECUTABLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.github.shpakovski.MASShortcut.${PRODUCT_NAME:rfc1034identifier}</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+</dict>
+</plist>
diff --git a/Tests/Prefix.pch b/Tests/Prefix.pch
new file mode 100644
index 0000000..f3bf22d
--- /dev/null
+++ b/Tests/Prefix.pch
@@ -0,0 +1,5 @@
+#ifdef __OBJC__
+ #import <Cocoa/Cocoa.h>
+ #import <XCTest/XCTest.h>
+ #import "Shortcut.h"
+#endif