diff options
| author | Tomáš Znamenáček | 2015-01-08 12:00:53 +0100 | 
|---|---|---|
| committer | Tomáš Znamenáček | 2015-01-08 12:00:53 +0100 | 
| commit | 9b919cba51e4cd11b0c4424930d6c18a1baec73c (patch) | |
| tree | a8107774609d5f4263f7b79749d93e6c6ff2642d | |
| parent | a3a459b4e4e47bf18dccd5dc7f315389346e3d6c (diff) | |
| parent | ea69d5939511f61a7082ba1e8ff46d247862a3fa (diff) | |
| download | MASShortcut-9b919cba51e4cd11b0c4424930d6c18a1baec73c.tar.bz2 | |
Merge pull request #53 from zoul/2.0-candidate
Thank you very much!
45 files changed, 2865 insertions, 907 deletions
| @@ -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 @@ -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: @@ -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 | 
