├── Framework ├── include │ ├── Shortcut.h │ ├── MASKeyCodes.h │ ├── MASShortcut.h │ ├── MASHotKey.h │ ├── MASLocalization.h │ ├── MASShortcutView.h │ ├── MASShortcutMonitor.h │ ├── MASShortcutValidator.h │ ├── MASShortcutView+Bindings.h │ ├── MASShortcutViewButtonCell.h │ ├── MASShortcutBinder.h │ ├── MASDictionaryTransformer.h │ └── module.modulemap ├── Prefix.pch ├── MASShortcut.modulemap ├── UI │ ├── MASShortcutViewButtonCell.h │ ├── MASLocalization.h │ ├── MASShortcutViewButtonCell.m │ ├── MASShortcutView.h │ ├── MASShortcutView+Bindings.h │ ├── MASShortcutView+Bindings.m │ ├── MASLocalization.m │ └── MASShortcutView.m ├── Shortcut.h ├── Monitoring │ ├── MASHotKey.h │ ├── MASHotKeyTests.m │ ├── MASShortcutMonitor.h │ ├── MASShortcutMonitorTests.m │ ├── MASHotKey.m │ └── MASShortcutMonitor.m ├── Info.plist ├── User Defaults Storage │ ├── MASDictionaryTransformer.h │ ├── MASDictionaryTransformerTests.m │ ├── MASDictionaryTransformer.m │ ├── MASShortcutBinder.h │ ├── MASShortcutBinder.m │ └── MASShortcutBinderTests.m ├── Model │ ├── MASShortcutTests.m │ ├── MASShortcutValidator.h │ ├── MASKeyCodes.h │ ├── MASShortcut.h │ ├── MASShortcutValidator.m │ └── MASShortcut.m └── Resources │ ├── zh-Hans.lproj │ └── Localizable.strings │ ├── zh-Hant.lproj │ └── Localizable.strings │ ├── ko.lproj │ └── Localizable.strings │ ├── ja.lproj │ └── Localizable.strings │ ├── pl.lproj │ └── Localizable.strings │ ├── cs.lproj │ └── Localizable.strings │ ├── en.lproj │ └── Localizable.strings │ ├── pt.lproj │ └── Localizable.strings │ ├── ru.lproj │ └── Localizable.strings │ ├── nl.lproj │ └── Localizable.strings │ ├── de.lproj │ └── Localizable.strings │ ├── fr.lproj │ └── Localizable.strings │ ├── sv.lproj │ └── Localizable.strings │ ├── es.lproj │ └── Localizable.strings │ └── it.lproj │ └── Localizable.strings ├── Demo ├── Prefix.pch ├── screenshot.png ├── main.m ├── cs.lproj │ └── Localizable.strings ├── ja.lproj │ └── Localizable.strings ├── AppDelegate.h ├── de.lproj │ └── Localizable.strings ├── en.lproj │ └── Localizable.strings ├── pt.lproj │ └── Localizable.strings ├── Info.plist └── AppDelegate.m ├── .travis.yml ├── Tests ├── Prefix.pch └── Info.plist ├── Fastlane ├── Pluginfile └── Fastfile ├── Gemfile ├── Makefile ├── .gitignore ├── MASShortcut.podspec ├── Package.swift ├── LICENSE ├── HACKING.md ├── Spec.md ├── CHANGELOG.md ├── MASShortcut.xcodeproj ├── xcshareddata │ └── xcschemes │ │ ├── Demo.xcscheme │ │ └── MASShortcut.xcscheme └── project.pbxproj ├── Gemfile.lock └── README.md /Framework/include/Shortcut.h: -------------------------------------------------------------------------------- 1 | ../Shortcut.h -------------------------------------------------------------------------------- /Framework/include/MASKeyCodes.h: -------------------------------------------------------------------------------- 1 | ../Model/MASKeyCodes.h -------------------------------------------------------------------------------- /Framework/include/MASShortcut.h: -------------------------------------------------------------------------------- 1 | ../Model/MASShortcut.h -------------------------------------------------------------------------------- /Framework/include/MASHotKey.h: -------------------------------------------------------------------------------- 1 | ../Monitoring/MASHotKey.h -------------------------------------------------------------------------------- /Framework/include/MASLocalization.h: -------------------------------------------------------------------------------- 1 | ../UI/MASLocalization.h -------------------------------------------------------------------------------- /Framework/include/MASShortcutView.h: -------------------------------------------------------------------------------- 1 | ../UI/MASShortcutView.h -------------------------------------------------------------------------------- /Framework/include/MASShortcutMonitor.h: -------------------------------------------------------------------------------- 1 | ../Monitoring/MASShortcutMonitor.h -------------------------------------------------------------------------------- /Framework/include/MASShortcutValidator.h: -------------------------------------------------------------------------------- 1 | ../Model/MASShortcutValidator.h -------------------------------------------------------------------------------- /Framework/Prefix.pch: -------------------------------------------------------------------------------- 1 | #import 2 | #import -------------------------------------------------------------------------------- /Framework/include/MASShortcutView+Bindings.h: -------------------------------------------------------------------------------- 1 | ../UI/MASShortcutView+Bindings.h -------------------------------------------------------------------------------- /Framework/include/MASShortcutViewButtonCell.h: -------------------------------------------------------------------------------- 1 | ../UI/MASShortcutViewButtonCell.h -------------------------------------------------------------------------------- /Demo/Prefix.pch: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | -------------------------------------------------------------------------------- /Framework/include/MASShortcutBinder.h: -------------------------------------------------------------------------------- 1 | ../User Defaults Storage/MASShortcutBinder.h -------------------------------------------------------------------------------- /Demo/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocoabits/MASShortcut/HEAD/Demo/screenshot.png -------------------------------------------------------------------------------- /Framework/include/MASDictionaryTransformer.h: -------------------------------------------------------------------------------- 1 | ../User Defaults Storage/MASDictionaryTransformer.h -------------------------------------------------------------------------------- /Framework/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module MASShortcut { 2 | header "Shortcut.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Demo/main.m: -------------------------------------------------------------------------------- 1 | int main(int argc, char *argv[]) 2 | { 3 | return NSApplicationMain(argc, (const char **)argv); 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | script: 3 | - xcodebuild -scheme MASShortcut build test 4 | - bundle exec pod lib lint 5 | -------------------------------------------------------------------------------- /Tests/Prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #import 4 | #import "Shortcut.h" 5 | #endif 6 | -------------------------------------------------------------------------------- /Demo/cs.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Feedback that’s displayed when user presses the sample shortcut. */ 2 | "Shortcut pressed!" = "Funguje!"; 3 | 4 | -------------------------------------------------------------------------------- /Demo/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Feedback that’s displayed when user presses the sample shortcut. */ 2 | "Shortcut pressed!" = "ショートカットが押されました!"; 3 | 4 | -------------------------------------------------------------------------------- /Fastlane/Pluginfile: -------------------------------------------------------------------------------- 1 | # Autogenerated by fastlane 2 | # 3 | # Ensure this file is checked in to source control! 4 | 5 | gem 'fastlane-plugin-changelog' 6 | -------------------------------------------------------------------------------- /Framework/MASShortcut.modulemap: -------------------------------------------------------------------------------- 1 | framework module MASShortcut { 2 | umbrella header "Shortcut.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | @interface AppDelegate : NSObject 2 | 3 | @property (nonatomic, assign) IBOutlet NSWindow *window; 4 | 5 | @end 6 | -------------------------------------------------------------------------------- /Demo/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Feedback that’s displayed when user presses the sample shortcut. */ 2 | "Shortcut pressed!" = "Kurzbefehl gedrückt!"; 3 | 4 | -------------------------------------------------------------------------------- /Demo/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Feedback that’s displayed when user presses the sample shortcut. */ 2 | "Shortcut pressed!" = "Shortcut pressed!"; 3 | 4 | -------------------------------------------------------------------------------- /Demo/pt.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Feedback that’s displayed when user presses the sample shortcut. */ 2 | "Shortcut pressed!" = "Atalho pressionado!"; 3 | 4 | -------------------------------------------------------------------------------- /Framework/UI/MASShortcutViewButtonCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface MASShortcutViewButtonCell : NSButtonCell 6 | 7 | @end 8 | 9 | NS_ASSUME_NONNULL_END 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'cocoapods' 3 | gem 'fastlane' 4 | 5 | plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') 6 | eval_gemfile(plugins_path) if File.exist?(plugins_path) 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: release 2 | 3 | ifndef BUILDDIR 4 | BUILDDIR := $(shell mktemp -d "$(TMPDIR)/MASShortcut.XXXXXX") 5 | endif 6 | 7 | release: 8 | xcodebuild -scheme MASShortcut -configuration Release -derivedDataPath "$(BUILDDIR)" build 9 | open "$(BUILDDIR)/Build/Products/Release" 10 | 11 | -------------------------------------------------------------------------------- /Framework/Shortcut.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "MASShortcut.h" 3 | #import "MASShortcutValidator.h" 4 | #import "MASShortcutMonitor.h" 5 | #import "MASShortcutBinder.h" 6 | #import "MASDictionaryTransformer.h" 7 | #import "MASShortcutView.h" 8 | #import "MASShortcutView+Bindings.h" 9 | #import "MASShortcutViewButtonCell.h" 10 | -------------------------------------------------------------------------------- /Framework/Monitoring/MASHotKey.h: -------------------------------------------------------------------------------- 1 | #import "MASShortcut.h" 2 | 3 | extern FourCharCode const MASHotKeySignature; 4 | 5 | @interface MASHotKey : NSObject 6 | 7 | @property(readonly) UInt32 carbonID; 8 | @property(copy) dispatch_block_t action; 9 | 10 | + (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | build/* 3 | *.pbxuser 4 | !default.pbxuser 5 | *.mode1v3 6 | !default.mode1v3 7 | *.mode2v3 8 | !default.mode2v3 9 | *.perspectivev3 10 | !default.perspectivev3 11 | *.xcworkspace 12 | !default.xcworkspace 13 | xcuserdata 14 | profile 15 | *.moved-aside 16 | .bundle 17 | Fastlane/README.md 18 | Fastlane/report.xml 19 | # Finder 20 | .DS_Store 21 | .swiftpm 22 | .build 23 | -------------------------------------------------------------------------------- /Framework/Monitoring/MASHotKeyTests.m: -------------------------------------------------------------------------------- 1 | #import "MASHotKey.h" 2 | 3 | @interface MASHotKeyTests : XCTestCase 4 | @end 5 | 6 | @implementation MASHotKeyTests 7 | 8 | - (void) testBasicFunctionality 9 | { 10 | MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut: 11 | [MASShortcut shortcutWithKeyCode:kVK_ANSI_H modifierFlags:NSEventModifierFlagCommand|NSEventModifierFlagOption]]; 12 | XCTAssertNotNil(hotKey, @"Register a simple Cmd-Alt-H hotkey."); 13 | } 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Framework/UI/MASLocalization.h: -------------------------------------------------------------------------------- 1 | /** 2 | Reads a localized string from the framework’s bundle. 3 | 4 | Normally you would use NSLocalizedString to read the localized 5 | strings, but that’s just a shortcut for loading the strings from 6 | the main bundle. And once the framework ends up in an app, the 7 | main bundle will be the app’s bundle and won’t contain our strings. 8 | So we introduced this helper function that makes sure to load the 9 | strings from the framework’s bundle. Please avoid using 10 | NSLocalizedString throughout the framework, it wouldn’t work 11 | properly. 12 | */ 13 | NSString *MASLocalizedString(NSString *key, NSString *comment); 14 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 2.4.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 2.4.0 21 | 22 | 23 | -------------------------------------------------------------------------------- /Framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 2.4.0 19 | CFBundleVersion 20 | 2.4.0 21 | NSHumanReadableCopyright 22 | Copyright © Vadim Shpakovski. All rights reserved. 23 | 24 | 25 | -------------------------------------------------------------------------------- /Framework/User Defaults Storage/MASDictionaryTransformer.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | extern NSString *const MASDictionaryTransformerName; 4 | 5 | /** 6 | Converts shortcuts for storage in user defaults. 7 | 8 | User defaults can’t stored custom types directly, they have to 9 | be serialized to `NSData` or some other supported type like an 10 | `NSDictionary`. In Cocoa Bindings, the conversion can be done 11 | using value transformers like this one. 12 | 13 | There’s a built-in transformer (`NSKeyedUnarchiveFromDataTransformerName`) 14 | that converts any `NSCoding` types to `NSData`, but with shortcuts 15 | it makes sense to use a dictionary instead – the defaults look better 16 | when inspected with the `defaults` command-line utility and the 17 | format is compatible with an older shortcut library called Shortcut 18 | Recorder. 19 | */ 20 | @interface MASDictionaryTransformer : NSValueTransformer 21 | @end 22 | -------------------------------------------------------------------------------- /Framework/Monitoring/MASShortcutMonitor.h: -------------------------------------------------------------------------------- 1 | #import "MASShortcut.h" 2 | 3 | /** 4 | Executes action when a shortcut is pressed. 5 | 6 | There can only be one instance of this class, otherwise things 7 | will probably not work. (There’s a Carbon event handler inside 8 | and there can only be one Carbon event handler of a given type.) 9 | */ 10 | @interface MASShortcutMonitor : NSObject 11 | 12 | - (instancetype) init __unavailable; 13 | + (instancetype) sharedMonitor; 14 | 15 | /** 16 | Register a shortcut along with an action. 17 | 18 | Attempting to insert an already registered shortcut probably won’t work. 19 | It may burn your house or cut your fingers. You have been warned. 20 | */ 21 | - (BOOL) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action; 22 | - (BOOL) isShortcutRegistered: (MASShortcut*) shortcut; 23 | 24 | - (void) unregisterShortcut: (MASShortcut*) shortcut; 25 | - (void) unregisterAllShortcuts; 26 | 27 | @end 28 | -------------------------------------------------------------------------------- /Framework/UI/MASShortcutViewButtonCell.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutViewButtonCell.h" 2 | 3 | @implementation MASShortcutViewButtonCell 4 | 5 | -(void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView 6 | { 7 | CGRect paddedFrame = cellFrame; 8 | 9 | //fix display on Big Sur 10 | if (@available(macOS 11, *)) { 11 | 12 | //fix vertical alignment 13 | paddedFrame.origin.y -= 1.0; 14 | 15 | //fix cancel button alignment 16 | if (self.alignment == NSTextAlignmentRight && 17 | (self.bezelStyle == NSBezelStyleTexturedRounded || 18 | self.bezelStyle == NSBezelStyleRounded)) { 19 | paddedFrame.size.width -= 14.0; 20 | 21 | if (self.bezelStyle == NSBezelStyleTexturedRounded) 22 | paddedFrame.origin.x += 7.0; 23 | } 24 | } 25 | 26 | [super drawInteriorWithFrame:paddedFrame inView:controlView]; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Framework/Monitoring/MASShortcutMonitorTests.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutMonitor.h" 2 | 3 | @interface MASShortcutMonitorTests : XCTestCase 4 | @end 5 | 6 | @implementation MASShortcutMonitorTests 7 | 8 | - (void) testMonitorCreation 9 | { 10 | XCTAssertNotNil([MASShortcutMonitor sharedMonitor], @"Create a shared shortcut monitor."); 11 | } 12 | 13 | - (void) testShortcutRegistration 14 | { 15 | MASShortcutMonitor *monitor = [MASShortcutMonitor sharedMonitor]; 16 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:kVK_ANSI_H modifierFlags:NSEventModifierFlagCommand|NSEventModifierFlagOption]; 17 | XCTAssertTrue([monitor registerShortcut:shortcut withAction:NULL], @"Register a shortcut."); 18 | XCTAssertTrue([monitor isShortcutRegistered:shortcut], @"Remember a previously registered shortcut."); 19 | [monitor unregisterShortcut:shortcut]; 20 | XCTAssertFalse([monitor isShortcutRegistered:shortcut], @"Forget shortcut after unregistering."); 21 | } 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /MASShortcut.podspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | Pod::Spec.new do |s| 3 | s.name = 'MASShortcut' 4 | s.version = '2.4.1' 5 | s.summary = 'Modern framework for managing global keyboard shortcuts compatible with Mac App Store' 6 | s.homepage = 'https://github.com/shpakovski/MASShortcut' 7 | s.license = 'BSD 2-clause' 8 | s.authors = { 'Vadim Shpakovski' => 'vadim@shpakovski.com', 9 | 'Tomáš Znamenáček' => 'tomas.znamenacek@gmail.com' } 10 | 11 | s.platform = :osx 12 | s.osx.deployment_target = "10.10" 13 | s.source = { :git => 'https://github.com/shpakovski/MASShortcut.git', :tag => s.version } 14 | s.source_files = 'Framework/**/*.{h,m}' 15 | s.exclude_files = 'Framework/**/*Tests.m' 16 | s.osx.frameworks = 'Carbon', 'AppKit' 17 | s.requires_arc = true 18 | s.osx.resource_bundles = { 'MASShortcut' => ['Resources/*.lproj'] } 19 | end 20 | -------------------------------------------------------------------------------- /Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 2.4.0 19 | CFBundleVersion 20 | 2.4.0 21 | LSMinimumSystemVersion 22 | ${MACOSX_DEPLOYMENT_TARGET} 23 | NSHumanReadableCopyright 24 | Copyright © 2012–2015 Vadim Shpakovski. All rights reserved. 25 | NSMainNibFile 26 | MainMenu 27 | NSPrincipalClass 28 | NSApplication 29 | 30 | 31 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "MASShortcut", 7 | defaultLocalization: "en", 8 | platforms: [ 9 | .macOS(.v10_11), 10 | ], 11 | products: [ 12 | .library(name: "MASShortcut", 13 | targets: ["MASShortcut"]) 14 | ], 15 | targets: [ 16 | .target( 17 | name: "MASShortcut", 18 | path: "Framework", 19 | exclude: [ 20 | "Model/MASShortcutTests.m", 21 | "Monitoring/MASHotKeyTests.m", 22 | "Monitoring/MASShortcutMonitorTests.m", 23 | "User Defaults Storage/MASDictionaryTransformerTests.m", 24 | "User Defaults Storage/MASShortcutBinderTests.m", 25 | "Info.plist", 26 | "MASShortcut.modulemap", 27 | "Prefix.pch" 28 | ], 29 | resources: [ 30 | .process("Resources") 31 | ], 32 | publicHeadersPath: "include" 33 | ) 34 | ], 35 | swiftLanguageVersions: [.v5] 36 | ) 37 | -------------------------------------------------------------------------------- /Framework/Monitoring/MASHotKey.m: -------------------------------------------------------------------------------- 1 | #import "MASHotKey.h" 2 | 3 | FourCharCode const MASHotKeySignature = 'MASS'; 4 | 5 | @interface MASHotKey () 6 | @property(assign) EventHotKeyRef hotKeyRef; 7 | @property(assign) UInt32 carbonID; 8 | @end 9 | 10 | @implementation MASHotKey 11 | 12 | - (instancetype) initWithShortcut: (MASShortcut*) shortcut 13 | { 14 | self = [super init]; 15 | 16 | static UInt32 CarbonHotKeyID = 0; 17 | 18 | _carbonID = ++CarbonHotKeyID; 19 | EventHotKeyID hotKeyID = { .signature = MASHotKeySignature, .id = _carbonID }; 20 | 21 | OSStatus status = RegisterEventHotKey([shortcut carbonKeyCode], [shortcut carbonFlags], 22 | hotKeyID, GetEventDispatcherTarget(), 0, &_hotKeyRef); 23 | 24 | if (status != noErr) { 25 | return nil; 26 | } 27 | 28 | return self; 29 | } 30 | 31 | + (instancetype) registeredHotKeyWithShortcut: (MASShortcut*) shortcut 32 | { 33 | return [[self alloc] initWithShortcut:shortcut]; 34 | } 35 | 36 | - (void) dealloc 37 | { 38 | if (_hotKeyRef) { 39 | UnregisterEventHotKey(_hotKeyRef); 40 | _hotKeyRef = NULL; 41 | } 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Framework/UI/MASShortcutView.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @class MASShortcut, MASShortcutValidator; 4 | 5 | extern NSString * _Nonnull const MASShortcutBinding; 6 | 7 | typedef NS_ENUM(NSInteger, MASShortcutViewStyle) { 8 | MASShortcutViewStyleDefault = 0, // Height = 19 px 9 | MASShortcutViewStyleTexturedRect, // Height = 25 px 10 | MASShortcutViewStyleRounded, // Height = 43 px 11 | MASShortcutViewStyleFlat, 12 | MASShortcutViewStyleRegularSquare 13 | }; 14 | 15 | @interface MASShortcutView : NSView 16 | 17 | @property (nonatomic, strong, nullable) MASShortcut *shortcutValue; 18 | @property (nonatomic, strong, nullable) MASShortcutValidator *shortcutValidator; 19 | @property (nonatomic, getter = isRecording) BOOL recording; 20 | @property (nonatomic, getter = isEnabled) BOOL enabled; 21 | @property (nonatomic, copy, nullable) void (^shortcutValueChange)(MASShortcutView * _Nonnull sender); 22 | @property (nonatomic, assign) MASShortcutViewStyle style; 23 | 24 | /// Returns custom class for drawing control. 25 | + (nonnull Class)shortcutCellClass; 26 | 27 | - (void)setAcceptsFirstResponder:(BOOL)value; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Framework/UI/MASShortcutView+Bindings.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "MASShortcutView.h" 3 | 4 | /** 5 | A simplified interface to bind the recorder value to user defaults. 6 | 7 | You can bind the `shortcutValue` to user defaults using the standard 8 | `bind:toObject:withKeyPath:options:` call, but since that’s a lot to type 9 | and read, here’s a simpler option. 10 | 11 | Setting the `associatedUserDefaultsKey` binds the view’s shortcut value 12 | to the given user defaults key. You can supply a value transformer to convert 13 | values between user defaults and `MASShortcut`. If you don’t supply 14 | a transformer, the `NSUnarchiveFromDataTransformerName` will be used 15 | automatically. 16 | 17 | Set `associatedUserDefaultsKey` to `nil` to disconnect the binding. 18 | */ 19 | @interface MASShortcutView (Bindings) 20 | 21 | @property(copy, nullable) NSString *associatedUserDefaultsKey; 22 | 23 | - (void) setAssociatedUserDefaultsKey: (nullable NSString*) newKey withTransformer: (nullable NSValueTransformer*) transformer; 24 | - (void) setAssociatedUserDefaultsKey: (nullable NSString*) newKey withTransformerName: (nullable NSString*) transformerName; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | desc "Make a new release: Bump version number, update changelog and commit & tag the changes" 2 | lane :release do |options| 3 | 4 | # Make sure we don't commit any work-in-progress with the release 5 | ensure_git_status_clean 6 | 7 | # Read the release type from command-line arguments, default to patch 8 | release_type = options[:type] ? options[:type] : "patch" 9 | 10 | # Bump version number 11 | increment_version_number(bump_type: release_type) 12 | version_number = get_version_number(target: "MASShortcut") 13 | increment_build_number(build_number: version_number) 14 | 15 | # Update changelog with the version number and release date 16 | stamp_changelog(section_identifier: version_number) 17 | git_add(path: 'CHANGELOG.md') 18 | 19 | # Update CocoaPods version 20 | version_bump_podspec(path: "MASShortcut.podspec", bump_type: release_type) 21 | git_add(path: 'MASShortcut.podspec') 22 | 23 | # Commit and tag the release 24 | commit_version_bump( 25 | message: "Release #{version_number}", 26 | xcodeproj: "MASShortcut.xcodeproj", 27 | force: true) 28 | add_git_tag(tag: version_number) 29 | end 30 | 31 | desc "Push podspec to CocoaPods trunk" 32 | lane :trunk do |options| 33 | pod_push 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2013, Vadim Shpakovski 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Framework/Model/MASShortcutTests.m: -------------------------------------------------------------------------------- 1 | @interface MASShortcutTests : XCTestCase 2 | @end 3 | 4 | @implementation MASShortcutTests 5 | 6 | - (void) testEquality 7 | { 8 | MASShortcut *keyA = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSEventModifierFlagControl]; 9 | MASShortcut *keyB = [MASShortcut shortcutWithKeyCode:2 modifierFlags:NSEventModifierFlagControl]; 10 | MASShortcut *keyC = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSEventModifierFlagOption]; 11 | MASShortcut *keyD = [MASShortcut shortcutWithKeyCode:1 modifierFlags:NSEventModifierFlagControl]; 12 | XCTAssertTrue([keyA isEqual:keyA], @"Shortcut is equal to itself."); 13 | XCTAssertTrue([keyA isEqual:[keyA copy]], @"Shortcut is equal to its copy."); 14 | XCTAssertFalse([keyA isEqual:keyB], @"Shortcuts not equal when key codes differ."); 15 | XCTAssertFalse([keyA isEqual:keyC], @"Shortcuts not equal when modifier flags differ."); 16 | XCTAssertTrue([keyA isEqual:keyD], @"Shortcuts are equal when key codes and modifiers are."); 17 | XCTAssertFalse([keyA isEqual:@"foo"], @"Shortcut not equal to an object of a different class."); 18 | } 19 | 20 | - (void) testShortcutRecorderCompatibility 21 | { 22 | MASShortcut *key = [MASShortcut shortcutWithKeyCode:87 modifierFlags:1048576]; 23 | XCTAssertEqualObjects([key description], @"⌘5", @"Basic compatibility with the keycode & modifier combination used by Shortcut Recorder."); 24 | } 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Backward Compatibility 2 | 3 | Please note that this framework supports older OS X versions down to 10.10. When changing the code, be careful not to call any API functions not available in 10.10 or call them conditionally, only where supported. 4 | 5 | # Commit Messages 6 | 7 | Please use descriptive commit message. As an example, _Bug fix_ commit message doesn’t say much, while _Fix a memory-management bug in formatting code_ works much better. A [nice detailed article about writing commit messages](http://chris.beams.io/posts/git-commit/) is also available. 8 | 9 | # How to Release a New Version 10 | 11 | The release process is automated using [Fastlane](https://fastlane.tools). To install the tooling: 12 | 13 | ```bash 14 | bundle install 15 | ``` 16 | 17 | To mint a new release: 18 | 19 | ```bash 20 | bundle exec fastlane release 21 | ``` 22 | 23 | This will bump the version number, stamp the changelog, etc. By default, the command will produce a patch release. (MASShortcut uses [Semantic Versioning](http://semver.org/), so please read the docs if you’re not sure what the deal is.) If you want a minor or major bump: 24 | 25 | ```bash 26 | bundle exec fastlane release type:minor 27 | bundle exec fastlane release type:major 28 | ``` 29 | 30 | After pushing the release including tags (`git push --follow-tags`), don’t forget to publish the release in CocoaPods: 31 | 32 | ```bash 33 | bundle exec fastlane trunk 34 | ``` 35 | 36 | That’s it. Go have a beer or a cup of tea to celebrate. 37 | -------------------------------------------------------------------------------- /Framework/User Defaults Storage/MASDictionaryTransformerTests.m: -------------------------------------------------------------------------------- 1 | @interface MASDictionaryTransformerTests : XCTestCase 2 | @end 3 | 4 | @implementation MASDictionaryTransformerTests 5 | 6 | - (void) testErrorHandling 7 | { 8 | MASDictionaryTransformer *transformer = [MASDictionaryTransformer new]; 9 | XCTAssertNil([transformer transformedValue:nil], 10 | @"Decoding a shortcut from a nil dictionary returns nil."); 11 | XCTAssertNil([transformer transformedValue:(id)@"foo"], 12 | @"Decoding a shortcut from an invalid-type dictionary returns nil."); 13 | XCTAssertNil([transformer transformedValue:@{}], 14 | @"Decoding a shortcut from an empty dictionary returns nil."); 15 | XCTAssertNil([transformer transformedValue:@{@"keyCode":@"foo"}], 16 | @"Decoding a shortcut from a wrong-typed dictionary returns nil."); 17 | XCTAssertNil([transformer transformedValue:@{@"keyCode":@1}], 18 | @"Decoding a shortcut from an incomplete dictionary returns nil."); 19 | XCTAssertNil([transformer transformedValue:@{@"modifierFlags":@1}], 20 | @"Decoding a shortcut from an incomplete dictionary returns nil."); 21 | } 22 | 23 | - (void) testNilRepresentation 24 | { 25 | MASDictionaryTransformer *transformer = [MASDictionaryTransformer new]; 26 | XCTAssertEqualObjects([transformer reverseTransformedValue:nil], [NSDictionary dictionary], 27 | @"Store nil values as an empty dictionary."); 28 | XCTAssertNil([transformer transformedValue:[NSDictionary dictionary]], 29 | @"Load empty dictionary as nil."); 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /Framework/UI/MASShortcutView+Bindings.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutView+Bindings.h" 2 | 3 | @implementation MASShortcutView (Bindings) 4 | 5 | - (NSString*) associatedUserDefaultsKey 6 | { 7 | NSDictionary* bindingInfo = [self infoForBinding:MASShortcutBinding]; 8 | if (bindingInfo != nil) { 9 | NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey]; 10 | NSString *key = [keyPath stringByReplacingOccurrencesOfString:@"values." withString:@""]; 11 | return key; 12 | } else { 13 | return nil; 14 | } 15 | } 16 | 17 | - (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformer: (NSValueTransformer*) transformer 18 | { 19 | // Break previous binding if the new binding is nil 20 | if (newKey == nil) { 21 | [self unbind:MASShortcutBinding]; 22 | return; 23 | } 24 | 25 | NSDictionary *options = transformer ? 26 | @{NSValueTransformerBindingOption:transformer} : 27 | nil; 28 | 29 | [self bind:MASShortcutBinding 30 | toObject:[NSUserDefaultsController sharedUserDefaultsController] 31 | withKeyPath:[@"values." stringByAppendingString:newKey] 32 | options:options]; 33 | } 34 | 35 | - (void) setAssociatedUserDefaultsKey: (NSString*) newKey withTransformerName: (NSString*) transformerName 36 | { 37 | [self setAssociatedUserDefaultsKey:newKey withTransformer:[NSValueTransformer valueTransformerForName:transformerName]]; 38 | } 39 | 40 | - (void) setAssociatedUserDefaultsKey: (NSString*) newKey 41 | { 42 | [self setAssociatedUserDefaultsKey:newKey withTransformerName:NSKeyedUnarchiveFromDataTransformerName]; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Framework/Model/MASShortcutValidator.h: -------------------------------------------------------------------------------- 1 | #import "MASShortcut.h" 2 | 3 | /** 4 | This class is used by the recording control to tell which shortcuts are acceptable. 5 | 6 | There are two kinds of shortcuts that are not considered acceptable: shortcuts that 7 | are too simple (like single letter keys) and shortcuts that are already used by the 8 | operating system. 9 | */ 10 | @interface MASShortcutValidator : NSObject 11 | 12 | /** 13 | Set to `YES` if you want to accept Option-something shortcuts. 14 | 15 | `NO` by default, since Option-something shortcuts are often used by system, 16 | for example Option-G will type the © sign. This also applies to Option-Shift 17 | shortcuts – in other words, shortcut recorder will not accept shortcuts like 18 | Option-Shift-K by default. (Again, since Option-Shift-K inserts the Apple 19 | logo sign by default.) 20 | */ 21 | @property(assign) BOOL allowAnyShortcutWithOptionModifier; 22 | 23 | /** 24 | Set to `YES` if you want to accept shortcuts that override the Services menu 25 | item. 26 | 27 | `NO` by default. Set to `YES` to allow shortcuts to override key equivalents of 28 | Services menu items. This can prevent users from being confused when they can't 29 | find the conflicting menu item since menu items in the Services menu are not 30 | always visible. 31 | */ 32 | @property(assign) BOOL allowOverridingServicesShortcut; 33 | 34 | + (instancetype) sharedValidator; 35 | 36 | - (BOOL) isShortcutValid: (MASShortcut*) shortcut; 37 | - (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation; 38 | - (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation; 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Framework/User Defaults Storage/MASDictionaryTransformer.m: -------------------------------------------------------------------------------- 1 | #import "MASDictionaryTransformer.h" 2 | #import "MASShortcut.h" 3 | 4 | NSString *const MASDictionaryTransformerName = @"MASDictionaryTransformer"; 5 | 6 | static NSString *const MASKeyCodeKey = @"keyCode"; 7 | static NSString *const MASModifierFlagsKey = @"modifierFlags"; 8 | 9 | @implementation MASDictionaryTransformer 10 | 11 | + (BOOL) allowsReverseTransformation 12 | { 13 | return YES; 14 | } 15 | 16 | // Storing nil values as an empty dictionary lets us differ between 17 | // “not available, use default value” and “explicitly set to none”. 18 | // See http://stackoverflow.com/questions/5540760 for details. 19 | - (NSDictionary*) reverseTransformedValue: (MASShortcut*) shortcut 20 | { 21 | if (shortcut == nil) { 22 | return [NSDictionary dictionary]; 23 | } else { 24 | return @{ 25 | MASKeyCodeKey: @([shortcut keyCode]), 26 | MASModifierFlagsKey: @([shortcut modifierFlags]) 27 | }; 28 | } 29 | } 30 | 31 | - (MASShortcut*) transformedValue: (NSDictionary*) dictionary 32 | { 33 | // We have to be defensive here as the value may come from user defaults. 34 | if (![dictionary isKindOfClass:[NSDictionary class]]) { 35 | return nil; 36 | } 37 | 38 | id keyCodeBox = [dictionary objectForKey:MASKeyCodeKey]; 39 | id modifierFlagsBox = [dictionary objectForKey:MASModifierFlagsKey]; 40 | 41 | SEL integerValue = @selector(integerValue); 42 | if (![keyCodeBox respondsToSelector:integerValue] || ![modifierFlagsBox respondsToSelector:integerValue]) { 43 | return nil; 44 | } 45 | 46 | return [MASShortcut 47 | shortcutWithKeyCode:[keyCodeBox integerValue] 48 | modifierFlags:[modifierFlagsBox integerValue]]; 49 | } 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Framework/Resources/zh-Hans.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "取消"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "点击以记录新快捷键"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "删除快捷键"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "键盘快捷键"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "好"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "记录快捷键"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "快捷键已清除"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "快捷键已设置"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "空格键"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "按键组合“%@”无法使用"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "当前按键组合无法使用,因为它已经用作其他系统全局快捷键。\n如果您真的想使用这个按键组合,大部分系统全局快捷键能在“系统偏好设置”里的“键盘”和“鼠标”面板中重设。"; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "当前快捷键无法使用,因为它已用作菜单项“%@”的快捷键。"; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "若要记录新的快捷键,单击此按钮,然后键入新的快捷键,或者按“delete键”删除已经存在的快捷键。"; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "键入新快捷键"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "键入快捷键"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "还原快捷键"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/zh-Hant.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "取消"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "按一下以記錄新快速鍵"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "刪除快速鍵"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "鍵盤快速鍵"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "好"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "記錄快速鍵"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "快速鍵已清除"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "快速鍵已設定"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "空格鍵"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "按鍵組合「%@」無法使用"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "目前按鍵組合無法使用,因為它已經用作其他系統全域快速鍵。\n如果您真的想使用此按鍵組合,大部分的系統全域快速鍵可在「系統偏好設定」裡的「鍵盤」和「滑鼠」面板中變更。"; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "目前快速鍵無法使用,因為它已經用作選單項目「%@」的快速鍵。"; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "若要記錄新的快速鍵,按一下此按鈕,然後輸入新的快速鍵,或者按「delete」鍵刪除已經存在的快速鍵。"; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "輸入新快速鍵"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "輸入快速鍵"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "還原快速鍵"; 48 | -------------------------------------------------------------------------------- /Framework/UI/MASLocalization.m: -------------------------------------------------------------------------------- 1 | #import "MASLocalization.h" 2 | #import "MASShortcut.h" 3 | 4 | static NSString *const MASLocalizationTableName = @"Localizable"; 5 | static NSString *const MASPlaceholderLocalizationString = @"XXX"; 6 | 7 | // The CocoaPods trickery here is needed because when the code 8 | // is built as a part of CocoaPods, it won’t make a separate framework 9 | // and the Localized.strings file won’t be bundled correctly. 10 | // See https://github.com/shpakovski/MASShortcut/issues/74 11 | NSString *MASLocalizedString(NSString *key, NSString *comment) { 12 | static NSBundle *localizationBundle = nil; 13 | static dispatch_once_t onceToken; 14 | dispatch_once(&onceToken, ^{ 15 | #if SWIFT_PACKAGE 16 | localizationBundle = SWIFTPM_MODULE_BUNDLE; 17 | #else 18 | NSBundle *frameworkBundle = [NSBundle bundleForClass:[MASShortcut class]]; 19 | // first we'll check if resources bundle was copied to MASShortcut framework bundle when !use_frameworks option is active 20 | NSURL *cocoaPodsBundleURL = [frameworkBundle URLForResource:@"MASShortcut" withExtension:@"bundle"]; 21 | if (cocoaPodsBundleURL) { 22 | localizationBundle = [NSBundle bundleWithURL: cocoaPodsBundleURL]; 23 | } else { 24 | // trying to fetch cocoapods bundle from main bundle 25 | cocoaPodsBundleURL = [[NSBundle mainBundle] URLForResource: @"MASShortcut" withExtension:@"bundle"]; 26 | if (cocoaPodsBundleURL) { 27 | localizationBundle = [NSBundle bundleWithURL: cocoaPodsBundleURL]; 28 | } else { 29 | // fallback to framework bundle 30 | localizationBundle = frameworkBundle; 31 | } 32 | } 33 | #endif 34 | }); 35 | return [localizationBundle localizedStringForKey:key 36 | value:MASPlaceholderLocalizationString 37 | table:MASLocalizationTableName]; 38 | } 39 | -------------------------------------------------------------------------------- /Framework/Resources/ko.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "취소"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "클릭해 단축 키를 입력"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "단축키 삭제"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "키보드 단축키"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "좋아"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "단축키 입력"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "단축키 삭제됨"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "단축키 설정됨"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "스페이스 바"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "%@ 단축키로 설정할 수 없습니다"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "이 결합은 시스템 전체에서 사용 때문에 단축키로 설정 할 수 없습니다.\n단축키를 사용하고 싶으면 시스템 환경 설정의 키보드, 마우스 패널에서 이미 설정되어있는 단축키를 변경하십시오."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "이 단축키는 ‘%@’ 메뉴 아이템에 사용되고 있기 때문에 설정할 수 없습니다."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "이 버튼을 클릭하고 단축키를 입력하면 새로운 단축키가 설정됩니다. 또한 삭제 버튼을 누르면 단축키가 삭제됩니다."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "새 단축키 입력"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "단축키 입력"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "오래된 단축키를 사용"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/ja.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "キャンセルする"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "クリックしてショートカットを入力"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "ショートカットを削除"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "キーボードショートカット"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "ショートカットを入力"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "ショートカットが削除されました"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "ショートカットが設定されました"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "スペース"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "%@ はショートカットに設定できません"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "このショートカットは、システム全体で使用されているショートカットのため、設定することができません。\nもしこのショートカットを使用したい場合、「システム環境設定」の「キーボード」、「マウス」のパネルから既に設定されているショートカットを変更してください。"; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "このショートカットは、メニュー操作の「%@」で使われているため、設定できません。"; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "このボタンをクリックし、ショートカットを入力すると、新しいショートカットが設定されます。また、削除ボタンをおすと、ショートカットが削除されます。"; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "ショートカットを入力"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "ショートカットを入力"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "古いショートカットを使用"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/pl.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Anuluj"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Kliknij, by ustawić nowy skrót"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Usuń skrót"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "skrót klawiszowy"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Utwórz skrót"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Skrót usunięty"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Skrót ustawiony"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Spacja"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "Nie można użyć kombinacji klawiszy %@"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Nie można użyć tej kombinacji, ponieważ jest już zajęta przez skrót systemowy.\nMożesz to zmienić w panelu Klawiatura w Preferencjach systemowych."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Ten skrót nie może być użyty, ponieważ w menu ma już przypisaną funkcję ‘%@’."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Aby ustawić nowy skrót, użyj tego przycisku, a następnie wpisz nowy skrót albo naciśnij klawisz delete, by usunąć istniejący skrót"; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Wpisz nowy skrót"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Wpisz skrót"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Użyj starego skrótu"; -------------------------------------------------------------------------------- /Framework/Resources/cs.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Zrušit"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Kliknutím nahrajete novou zkratku"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Smazat zkratku"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "klávesová zkratka"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Nahrát zkratku"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "zkratka smazána"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "zkratka nastavena"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Mezerník"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "Kombinace %@ se nedá použít"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Tato zkratka se nedá použít, protože už ji obsadil systém.\nKdybyste na ní trvali, většina systémových zkratek se dá přenastavit v Předvolbách systému."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Tato zkratka se nedá použít, protože už je použita pro menu (%@)."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Pokud chcete nahrát novou zkratku, stiskněte toto tlačítko a následně vybranou zkratku. Stisknutím Delete vymažete stávající zkratku."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Stiskněte zkratku"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Stiskněte zkratku"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Vrátit se k původní"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/en.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Cancel"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Click to record new shortcut"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Delete shortcut"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "keyboard shortcut"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Record Shortcut"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Shortcut cleared"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Shortcut set"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Space"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "The key combination %@ cannot be used"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "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."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "This shortcut cannot be used because it is already used by the menu item ‘%@’."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Type New Shortcut"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Type Shortcut"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Use Old Shortcut"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/pt.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Cancelar"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" ="Clique para gravar o atalho"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Apagar atalho"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "atalho de teclado"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Gravar Atalho"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Atalho limpo"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Atalho definido"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Espaço"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "A combinação de teclas “%@” não pode ser usada"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Esta combinação não pode ser usada porque ela já é usada por um atalho global do sistema.\nA maioria dos atalhos pode ser alterada no painel Teclado das Preferências do Sistema, caso realmente deseje usar esta combinação."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Este atalho não pode ser usado porque ele já é usado pelo item de menu “%@”."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Para gravar um atalho novo, clique neste botão e digite o novo atalho ou pressione apagar para limpar um atalho existente."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Digite o atalho"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Digite o atalho"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Usar atalho antigo"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/ru.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Отмена"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Нажмите для записи сочетания клавиш"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Удалить горячую клавишу"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "сочетание клавиш"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "ОК"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Ввести сочетание"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Сочетание клавиш удалено"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Сочетание клавиш назначено"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Пробел"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "Нельзя использовать сочетание клавиш %@"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Нельзя использовать это сочетание клавиш, потому что оно уже используется в системе.\n Если вы хотите использовать это сочетание, измените существующее системное сочетание клавиш через панель Клавиатура в Cистемных настройках."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Нельзя использовать это сочетание, потому что оно уже связано с элементом ‘%@’."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Чтобы назначить новое сочетание клавиш, нажмите эту кнопку и введите новое сочетание, или нажмите \"Удалить\", чтобы удалить действующее сочетание клавиш."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Введите сочетание"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Введите сочетание"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Вернуть старое"; -------------------------------------------------------------------------------- /Framework/Resources/nl.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Verbreken"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Druk om een nieuwe sneltoets in te voeren"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Verwijder sneltoets"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "sneltoets"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Sneltoets opnemen"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Sneltoets verwijderd"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Sneltoets zetten"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Spatie"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "De sneltoetsencombinatie kan niet worden gebruikt"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Deze combinatie kan niet worden gebruikt want hij wordt al gebruikt door een systeembreed sneltoets.\nAls je deze combinatie echt wilt gebruiken, kun je de meeste sneltoetsen binnen Toetsenbordinstellingen veranderen."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Deze sneltoets kan niet worden gebruikt want hij wordt al gebruikt door het menu item ‘%@’."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Om nieuwe sneltoets op te nemen, druk op deze knop, en voer een nieuwe sneltoets in, of druk op verwijder om bestaande sneltoets te verwijderen."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Voer Nieuwe Sneltoets in"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Voer Sneltoets in"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Gebruik Oude Sneltoets"; -------------------------------------------------------------------------------- /Framework/Resources/de.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Abbrechen"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Klicken um neuen Kurzbefehl aufzunehmen"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Kurzbefehl Löschen"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "Kurzbefehl"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Kurzbefehl aufnehmen"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Kurzbefehl gelöscht"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Kurzbefehl gesetzt"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Leertaste"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "Die Tastenkombination %@ kann nicht genutzt werden"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Diese Kombination kann nicht genutzt werden, weil sie bereits als systemweiter Kurzbefehl genutzt wird.\nFalls du diese Tastenkombination wirklich benutzen willst, können die meisten Kurzbefehle in den Tastatur Systemeinstellungen geändert werden."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Dieser Kurzbefehl kann nicht genutzt werden, weil er bereits vom Menüpunkt „%@“ genutzt wird."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Drücke diesen Button, um einen neuen Kurzbefehl aufzunehmen. Tippe dann den neuen Kurzbefehl oder drücke Löschen, um den aktuellen Kurzbefehl zu löschen."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Neuen eingeben"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Kurzbefehl eingeben"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Alten nutzen"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/fr.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Annuler"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Cliquez pour enregistrer le raccourci"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Supprimer le raccourci"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "raccourci clavier"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Enregistrer le raccourci"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Raccourci supprimé"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Raccourci configuré"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Espace"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "La combinaison %@ ne peut être utilisée"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Cette combinaison de touches ne peut être utilisée parce qu’elle est réservée pour un raccourci du système.\nSi vous désirez l’utiliser, la plupart des raccourcis peuvent être modifiés dans l’onglet Clavier, dans Préférences Système."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Ce raccourci ne peut être utilisé parce qu’il est déjà utilisé par le point de menu «%@»."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Pour enregistrer un nouveau raccourci, cliquez sur ce bouton et tapez le nouveau raccourci, ou bien, tapez sur «Supprimer» pour supprimer le raccourci configuré."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Saisir un raccourci"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Saisir un raccourci"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Revenir au raccourci précédent"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/sv.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Avbryt"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Klicka för att registrera ny kortkommando"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Ta bort en kortkommando"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "Tangentbordskortkommando"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Registrera kortkommando"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Kortkommando rensas"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Kortkommando uppsättning"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Utrymme"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "Tangentkombinationen %@ kan inte användas"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Den här kombinationen kan inte användas eftersom den redan används som en tangentbordskortkommando. Om du verkligen vill använda den här tangentkombinationen kan de flesta genvägar ändras under Tangentbord & Mus i Systeminställningar."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Denna kortkommando kan inte användas eftersom det redan används av ett menyalternativ ‘%@’."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "För att registrera en ny kortkommando, klicka på den här knappen och skriv sedan in den nya kortkommando, eller tryck på radera för att rensa en befintlig kortkommando."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Skriv Ny Kortkommando"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Skriv Kortkommando"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Använd Gammal Kortkommando"; 48 | -------------------------------------------------------------------------------- /Framework/Resources/es.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Cancelar"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Haga clic para grabar nuevo atajo"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Borrar atajo"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "función rápida de teclado"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Grabar Función rápida"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Función rápida eliminada"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Función rápida creada"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Espacio"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "La combinación de teclas %@ no puede ser utilizada"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Esta combinación no puede ser utilizar debido a que es una función rápida del sistema.\nSi realmente desea utilizar esta combinación de teclas, la mayoría de las funciones rápidas se puede cambiar en el Panel de Teclado en las Preferencias del sistema."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Esta función rápida no se puede utilizar debido a que ya está siendo utilizada por el elemento de menú '%@'."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Para grabar una nueva función rápida, haga clic en este botón, y luego teclee la nueva función rápida, o pulse borrar para quitar una función rápida existente."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Teclee la nueva función rápida"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Teclee la función rápida"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Usar una función rápida previa"; 48 | -------------------------------------------------------------------------------- /Framework/Model/MASKeyCodes.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | // These glyphs are missed in Carbon.h 5 | typedef NS_ENUM(unsigned short, kMASShortcutGlyph) { 6 | kMASShortcutGlyphEject = 0x23CF, 7 | kMASShortcutGlyphClear = 0x2715, 8 | kMASShortcutGlyphDeleteLeft = 0x232B, 9 | kMASShortcutGlyphDeleteRight = 0x2326, 10 | kMASShortcutGlyphLeftArrow = 0x2190, 11 | kMASShortcutGlyphRightArrow = 0x2192, 12 | kMASShortcutGlyphUpArrow = 0x2191, 13 | kMASShortcutGlyphDownArrow = 0x2193, 14 | kMASShortcutGlyphEscape = 0x238B, 15 | kMASShortcutGlyphHelp = 0x003F, 16 | kMASShortcutGlyphPageDown = 0x21DF, 17 | kMASShortcutGlyphPageUp = 0x21DE, 18 | kMASShortcutGlyphTabRight = 0x21E5, 19 | kMASShortcutGlyphReturn = 0x2305, 20 | kMASShortcutGlyphReturnR2L = 0x21A9, 21 | kMASShortcutGlyphPadClear = 0x2327, 22 | kMASShortcutGlyphNorthwestArrow = 0x2196, 23 | kMASShortcutGlyphSoutheastArrow = 0x2198, 24 | }; 25 | 26 | // The missing function key definitions for `NS*FunctionKey`s 27 | typedef NS_ENUM(unsigned short, kMASShortcutFuctionKey) { 28 | kMASShortcutEscapeFunctionKey = 0x001B, 29 | kMASShortcutDeleteFunctionKey = 0x0008, 30 | kMASShortcutSpaceFunctionKey = 0x0020, 31 | kMASShortcutReturnFunctionKey = 0x000D, 32 | kMASShortcutTabFunctionKey = 0x0009, 33 | }; 34 | 35 | NS_INLINE NSString* NSStringFromMASKeyCode(unsigned short ch) 36 | { 37 | return [NSString stringWithFormat:@"%C", ch]; 38 | } 39 | 40 | NS_INLINE NSUInteger MASPickCocoaModifiers(NSUInteger flags) 41 | { 42 | return (flags & (NSEventModifierFlagControl | NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagCommand)); 43 | } 44 | 45 | // Used in `-[MASShortcutValidator isShortcut:alreadyTakenInMenu:explanation:]`. 46 | // This prevents incorrectly detecting an overlap with any shortcuts using the `fn` key. 47 | NS_INLINE NSUInteger MASPickModifiersIncludingFn(NSUInteger flags) 48 | { 49 | return (flags & (NSEventModifierFlagControl | NSEventModifierFlagShift | NSEventModifierFlagOption | NSEventModifierFlagCommand | NSEventModifierFlagFunction)); 50 | } 51 | 52 | NS_INLINE UInt32 MASCarbonModifiersFromCocoaModifiers(NSUInteger cocoaFlags) 53 | { 54 | return 55 | (cocoaFlags & NSEventModifierFlagCommand ? cmdKey : 0) 56 | | (cocoaFlags & NSEventModifierFlagOption ? optionKey : 0) 57 | | (cocoaFlags & NSEventModifierFlagControl ? controlKey : 0) 58 | | (cocoaFlags & NSEventModifierFlagShift ? shiftKey : 0); 59 | } 60 | -------------------------------------------------------------------------------- /Framework/User Defaults Storage/MASShortcutBinder.h: -------------------------------------------------------------------------------- 1 | #import "MASShortcutMonitor.h" 2 | 3 | /** 4 | Binds actions to user defaults keys. 5 | 6 | If you store shortcuts in user defaults (for example by binding 7 | a `MASShortcutView` to user defaults), you can use this class to 8 | connect an action directly to a user defaults key. If the shortcut 9 | stored under the key changes, the action will get automatically 10 | updated to the new one. 11 | 12 | This class is mostly a wrapper around a `MASShortcutMonitor`. It 13 | watches the changes in user defaults and updates the shortcut monitor 14 | accordingly with the new shortcuts. 15 | */ 16 | @interface MASShortcutBinder : NSObject 17 | 18 | /** 19 | A convenience shared instance. 20 | 21 | You may use it so that you don’t have to manage an instance by hand, 22 | but it’s perfectly fine to allocate and use a separate instance instead. 23 | */ 24 | + (instancetype) sharedBinder; 25 | 26 | /** 27 | The underlying shortcut monitor. 28 | */ 29 | @property(strong) MASShortcutMonitor *shortcutMonitor; 30 | 31 | /** 32 | Binding options customizing the access to user defaults. 33 | 34 | As an example, you can use `NSValueTransformerNameBindingOption` to customize 35 | the storage format used for the shortcuts. By default the shortcuts are converted 36 | from `NSData` (`NSKeyedUnarchiveFromDataTransformerName`). Note that if the 37 | binder is to work with `MASShortcutView`, both object have to use the same storage 38 | format. 39 | */ 40 | @property(copy) NSDictionary *bindingOptions; 41 | 42 | /** 43 | Binds given action to a shortcut stored under the given defaults key. 44 | 45 | In other words, no matter what shortcut you store under the given key, 46 | pressing it will always trigger the given action. 47 | */ 48 | - (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action; 49 | 50 | /** 51 | Disconnect the binding between user defaults and action. 52 | 53 | In other words, the shortcut stored under the given key will no longer trigger an action. 54 | */ 55 | - (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName; 56 | 57 | /** 58 | Register default shortcuts in user defaults. 59 | 60 | This is a convenience frontend to `[NSUserDefaults registerDefaults]`. 61 | The dictionary should contain a map of user defaults’ keys to appropriate 62 | keyboard shortcuts. The shortcuts will be transformed according to 63 | `bindingOptions` and registered using `registerDefaults`. 64 | */ 65 | - (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts; 66 | 67 | @end 68 | -------------------------------------------------------------------------------- /Framework/Resources/it.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* Cancel action button in recording state */ 2 | "Cancel" = "Annulla"; 3 | 4 | /* Tooltip for non-empty shortcut button */ 5 | "Click to record new shortcut" = "Fai clic per registrare una nuova abbreviazione"; 6 | 7 | /* Tooltip for hint button near the non-empty shortcut */ 8 | "Delete shortcut" = "Cancella abbreviazione"; 9 | 10 | /* VoiceOver title */ 11 | "keyboard shortcut" = "Abbreviazione da tastiera"; 12 | 13 | /* Alert button when shortcut is already used */ 14 | "OK" = "OK"; 15 | 16 | /* Empty shortcut button in normal state */ 17 | "Record Shortcut" = "Registra abbreviazione"; 18 | 19 | /* VoiceOver: Shortcut cleared */ 20 | "Shortcut cleared" = "Abbreviazione rimossa"; 21 | 22 | /* VoiceOver: Shortcut set */ 23 | "Shortcut set" = "Abbreviazione impostata"; 24 | 25 | /* Shortcut glyph name for SPACE key */ 26 | "Space" = "Spazio"; 27 | 28 | /* Title for alert when shortcut is already used */ 29 | "The key combination %@ cannot be used" = "Questa combinazione %@ di tasti non può essere usata"; 30 | 31 | /* Message for alert when shortcut is already used by the system */ 32 | "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." = "Questa combinazione di tasti non può essere usata perché già assegnata a un'abbreviazione da tastiera a livello di Sistema.\nSe vuoi davvero usare questa combinazione di tasti, puoi modificare la maggior parte delle abbreviazioni nei pannelli Tastiera e Mouse delle Preferenze di Sistema."; 33 | 34 | /* Message for alert when shortcut is already used */ 35 | "This shortcut cannot be used because it is already used by the menu item ‘%@’." = "Questa combinazione di tasti non può essere usata perché già usata dalla voce di menu ‘%@’."; 36 | 37 | /* VoiceOver shortcut help */ 38 | "To record a new shortcut, click this button, and then type the new shortcut, or press delete to clear an existing shortcut." = "Per registrare una nuova abbreviazione fai clic su questo pulsante, quindi inserisci i tasti della nuova abbreviazione o premi cancella per ripristinare un'abbreviazione esistente."; 39 | 40 | /* Non-empty shortcut button in recording state */ 41 | "Type New Shortcut" = "Digita nuova abbreviazione"; 42 | 43 | /* Empty shortcut button in recording state */ 44 | "Type Shortcut" = "Digita abbreviazione"; 45 | 46 | /* Cancel action button for non-empty shortcut in recording state */ 47 | "Use Old Shortcut" = "Usa abbreviazione precedente"; 48 | -------------------------------------------------------------------------------- /Spec.md: -------------------------------------------------------------------------------- 1 | This is an attempt to specify some of the parts of the library so that it’s easier to spot bugs and regressions. 2 | 3 | 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. 4 | 5 | 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. 6 | 7 | # Recording Shortcuts 8 | 9 | * If a shortcut has no modifiers and is not a function key (F1–F20), it must be rejected. (Examples: `A`, Shift-A.) 10 | * If the shortcut is plain Esc without modifiers, it must be rejected and cancels the recording. 11 | * If the shortcut is plain Backspace or plain Delete, it must be rejected, clears the recorded shortcut and cancels the recording. 12 | * 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. 13 | * 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”?) 14 | * TBD: Option-key handling. 15 | * All other shortcuts must be accepted. (Examples: Ctrl-Esc, Cmd-Delete, F16.) 16 | 17 | # Formatting Shortcuts 18 | 19 | On different keyboard layouts (such as US and Czech), a single shortcut (a combination of physical keys) may be formatted into different strings. 20 | 21 | For example, the default system shortcut for toggling directly to Space #2 is Control–2. But when you switch to the Czech keyboard layout, the physical key with the `2` label now inserts the `ě` character. Thus, on most keyboard layouts the shortcut for toggling to Space #2 is called `^2`, but on the Czech layout it’s called `^ě`. (I stress that this is the same combination of hardware keys and the same `MASShortcut` instance.) 22 | 23 | This is reflected by the system: When you open the System Preferences → Keyboard → Shortcuts pane, the shortcuts displayed depend on the currently selected keyboard layout (try switching between the US and Czech keyboard layouts and reopening the preference pane). 24 | 25 | This means that the identity of a shortcut is given by its key code and modifiers (such as `kVK_ANSI_2` and `NSControlKeyMask`), not the `keyCodeString` returned by the `MASShortcut` class. This string may change depending on the current keyboard layout: `^2` with the US keyboard active, but `^ě` with the Czech keyboard active. 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Details about this file’s format at . The change log is parsed automatically when minting releases through Fastlane, see `Fastlane/Fastfile`. 2 | 3 | ## [Unreleased] 4 | 5 | ## [2.4.0] - 2019-06-11 6 | 7 | - Use properly typedef’d enumerations to improve Swift syntax [Tony Arnold] 8 | - Fix return from a NSToolTip method that expects a non-null return value [Tony Arnold] 9 | - Brazilian Portuguese localization [vitu] 10 | - `kVK_ANSI_KeypadMinus` fixes [Vyacheslav Dubovitsky] 11 | - Swedish localization [Pavel Kozárek] 12 | - Provide intrinsicContenSize to improve compatibility with autolayout [Fletcher T. Penney] 13 | - Allow app extension API only for the framework target [zoul] 14 | - Make it possible to turn off shortcut validation [zoul] 15 | - Use more specific types for -keyCode and -modifierFlags [zoul] 16 | - Use NSEvent constants instead of hard-coded numbers [zoul] 17 | 18 | ## [2.3.6] - 2016-10-30 19 | - Improve compatibility with the 10.12 SDK [thanks to Clemens Schulz] 20 | 21 | ## [2.3.5] - 2016-9-7 22 | - Improve Italian localization [zoul] 23 | 24 | ## [2.3.4] - 2016-8-12 25 | - Simplified and traditional Chinese localization [MichaelRow] 26 | - Add Korean, Dutch, Polish, Russian & update Spanish localizations [Radek Pietruszewski] 27 | - Improved German localization [Florian Schliep] 28 | - Add a Makefile to improve command-line building [sfsam] 29 | 30 | ## [2.3.3] - 2016-1-9 31 | - Improved Japanese localization [oreshinya] 32 | - Improved Frech localization [magiclantern] 33 | - Fixed CocoaPods localization with `use_frameworks!` [nivanchikov] 34 | 35 | ## [2.3.2] - 2015-10-12 36 | - Fixed localization when building through CocoaPods [Allan Beaufour] 37 | 38 | ## [2.3.1] - 2015-9-10 39 | - Trying to work around a strange build error in CocoaPods. 40 | 41 | ## [2.3.0] - 2015-9-10 42 | - Basic localization support for Czech, German, Spanish, Italian, French, and Japanese. Native speaking testers welcome! 43 | 44 | ## [2.2.0] - 2015-8-18 45 | - Basic accessibility support [starkos] 46 | - Added an option to hide the shortcut delete button [oreshinya] 47 | - Advertised support for Carthage [Tom Brown] 48 | - Bugfix for shortcuts not working after set twice [Roman Sokolov] 49 | - Ignore a solo Tab key when recording shortcuts [Roman Sokolov] 50 | 51 | ## [2.1.2] - 2015-1-28 52 | - Better key equivalent handling for non-ASCII layouts. [Dmitry Obukhov] 53 | 54 | ## [2.1.1] - 2015-1-16 55 | - Another headerdoc fix for CocoaDocs, hopefully the last one. 56 | 57 | ## [2.1.0] - 2015-1-16 58 | - Added support for older OS X versions down to 10.6 included. 59 | - Headerdoc markup that plays better with CocoaDocs. 60 | 61 | ## [2.0.1] - 2015-1-9 62 | - Trivial Podspec fix. 63 | 64 | ## [2.0.0] - 2015-1-9 65 | - First version with a changes file :) 66 | - Major, backwards incompatible refactoring to simplify long-term maintenance. 67 | - Added a simple spec describing the recording behaviour. 68 | - Adds compatibility mode with Shortcut Recorder. 69 | -------------------------------------------------------------------------------- /Framework/Model/MASShortcut.h: -------------------------------------------------------------------------------- 1 | #import "MASKeyCodes.h" 2 | 3 | /** 4 | A model class to hold a key combination. 5 | 6 | This class just represents a combination of keys. It does not care if 7 | the combination is valid or can be used as a hotkey, it doesn’t watch 8 | the input system for the shortcut appearance, nor it does access user 9 | defaults. 10 | */ 11 | @interface MASShortcut : NSObject 12 | 13 | /** 14 | The virtual key code for the keyboard key. 15 | 16 | Hardware independent, same as in `NSEvent`. See `Events.h` in the HIToolbox 17 | framework for a complete list, or Command-click this symbol: `kVK_ANSI_A`. 18 | */ 19 | @property (nonatomic, readonly) NSInteger keyCode; 20 | 21 | /** 22 | Cocoa keyboard modifier flags. 23 | 24 | Same as in `NSEvent`: `NSCommandKeyMask`, `NSAlternateKeyMask`, etc. 25 | */ 26 | @property (nonatomic, readonly) NSEventModifierFlags modifierFlags; 27 | 28 | /** 29 | Same as `keyCode`, just a different type. 30 | */ 31 | @property (nonatomic, readonly) UInt32 carbonKeyCode; 32 | 33 | /** 34 | Carbon modifier flags. 35 | 36 | A bit sum of `cmdKey`, `optionKey`, etc. 37 | */ 38 | @property (nonatomic, readonly) UInt32 carbonFlags; 39 | 40 | /** 41 | A string representing the “key” part of a shortcut, like the `5` in `⌘5`. 42 | 43 | @warning The value may change depending on the active keyboard layout. 44 | For example for the `^2` keyboard shortcut (`kVK_ANSI_2+NSControlKeyMask` 45 | to be precise) the `keyCodeString` is `2` on the US keyboard, but `ě` when 46 | the Czech keyboard layout is active. See the spec for details. 47 | */ 48 | @property (nonatomic, readonly, nullable) NSString *keyCodeString; 49 | 50 | /** 51 | A key-code string used in key equivalent matching. 52 | 53 | For precise meaning of “key equivalents” see the `keyEquivalent` 54 | property of `NSMenuItem`. Here the string is used to support shortcut 55 | validation (“is the shortcut already taken in this menu?”) and 56 | for display in `NSMenu`. 57 | 58 | The value of this property may differ from `keyCodeString`. For example 59 | the Russian keyboard has a `Г` (Ge) Cyrillic character in place of the 60 | latin `U` key. This means you can create a `^Г` shortcut, but in menus 61 | that’s always displayed as `^U`. So the `keyCodeString` returns `Г` 62 | and `keyCodeStringForKeyEquivalent` returns `U`. 63 | */ 64 | @property (nonatomic, readonly, nullable) NSString *keyCodeStringForKeyEquivalent; 65 | 66 | /** 67 | A string representing the shortcut modifiers, like the `⌘` in `⌘5`. 68 | */ 69 | @property (nonatomic, readonly, nonnull) NSString *modifierFlagsString; 70 | 71 | - (nonnull instancetype)initWithKeyCode:(NSInteger)code modifierFlags:(NSEventModifierFlags)flags; 72 | + (nonnull instancetype)shortcutWithKeyCode:(NSInteger)code modifierFlags:(NSEventModifierFlags)flags; 73 | 74 | /** 75 | Creates a new shortcut from an `NSEvent` object. 76 | 77 | This is just a convenience initializer that reads the key code and modifiers from an `NSEvent`. 78 | */ 79 | + (nonnull instancetype)shortcutWithEvent:(nonnull NSEvent *)anEvent; 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /Framework/Monitoring/MASShortcutMonitor.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutMonitor.h" 2 | #import "MASHotKey.h" 3 | 4 | @interface MASShortcutMonitor () 5 | @property(assign) EventHandlerRef eventHandlerRef; 6 | @property(strong) NSMutableDictionary *hotKeys; 7 | @end 8 | 9 | static OSStatus MASCarbonEventCallback(EventHandlerCallRef, EventRef, void*); 10 | 11 | @implementation MASShortcutMonitor 12 | 13 | #pragma mark Initialization 14 | 15 | - (instancetype) init 16 | { 17 | self = [super init]; 18 | [self setHotKeys:[NSMutableDictionary dictionary]]; 19 | EventTypeSpec hotKeyPressedSpec = { .eventClass = kEventClassKeyboard, .eventKind = kEventHotKeyPressed }; 20 | OSStatus status = InstallEventHandler(GetEventDispatcherTarget(), MASCarbonEventCallback, 21 | 1, &hotKeyPressedSpec, (__bridge void*)self, &_eventHandlerRef); 22 | if (status != noErr) { 23 | return nil; 24 | } 25 | return self; 26 | } 27 | 28 | - (void) dealloc 29 | { 30 | if (_eventHandlerRef) { 31 | RemoveEventHandler(_eventHandlerRef); 32 | _eventHandlerRef = NULL; 33 | } 34 | } 35 | 36 | + (instancetype) sharedMonitor 37 | { 38 | static dispatch_once_t once; 39 | static MASShortcutMonitor *sharedInstance; 40 | dispatch_once(&once, ^{ 41 | sharedInstance = [[self alloc] init]; 42 | }); 43 | return sharedInstance; 44 | } 45 | 46 | #pragma mark Registration 47 | 48 | - (BOOL) registerShortcut: (MASShortcut*) shortcut withAction: (dispatch_block_t) action 49 | { 50 | MASHotKey *hotKey = [MASHotKey registeredHotKeyWithShortcut:shortcut]; 51 | if (hotKey) { 52 | [hotKey setAction:action]; 53 | [_hotKeys setObject:hotKey forKey:shortcut]; 54 | return YES; 55 | } else { 56 | return NO; 57 | } 58 | } 59 | 60 | - (void) unregisterShortcut: (MASShortcut*) shortcut 61 | { 62 | if (shortcut) { 63 | [_hotKeys removeObjectForKey:shortcut]; 64 | } 65 | } 66 | 67 | - (void) unregisterAllShortcuts 68 | { 69 | [_hotKeys removeAllObjects]; 70 | } 71 | 72 | - (BOOL) isShortcutRegistered: (MASShortcut*) shortcut 73 | { 74 | return !![_hotKeys objectForKey:shortcut]; 75 | } 76 | 77 | #pragma mark Event Handling 78 | 79 | - (void) handleEvent: (EventRef) event 80 | { 81 | if (GetEventClass(event) != kEventClassKeyboard) { 82 | return; 83 | } 84 | 85 | EventHotKeyID hotKeyID; 86 | OSStatus status = GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(hotKeyID), NULL, &hotKeyID); 87 | if (status != noErr || hotKeyID.signature != MASHotKeySignature) { 88 | return; 89 | } 90 | 91 | [_hotKeys enumerateKeysAndObjectsUsingBlock:^(MASShortcut *shortcut, MASHotKey *hotKey, BOOL *stop) { 92 | if (hotKeyID.id == [hotKey carbonID]) { 93 | if ([hotKey action]) { 94 | dispatch_async(dispatch_get_main_queue(), [hotKey action]); 95 | } 96 | *stop = YES; 97 | } 98 | }]; 99 | } 100 | 101 | @end 102 | 103 | static OSStatus MASCarbonEventCallback(EventHandlerCallRef _, EventRef event, void *context) 104 | { 105 | MASShortcutMonitor *dispatcher = (__bridge id)context; 106 | [dispatcher handleEvent:event]; 107 | return noErr; 108 | } 109 | -------------------------------------------------------------------------------- /MASShortcut.xcodeproj/xcshareddata/xcschemes/Demo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /Framework/User Defaults Storage/MASShortcutBinder.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutBinder.h" 2 | #import "MASShortcut.h" 3 | 4 | @interface MASShortcutBinder () 5 | @property(strong) NSMutableDictionary *actions; 6 | @property(strong) NSMutableDictionary *shortcuts; 7 | @end 8 | 9 | @implementation MASShortcutBinder 10 | 11 | #pragma mark Initialization 12 | 13 | - (id) init 14 | { 15 | self = [super init]; 16 | [self setActions:[NSMutableDictionary dictionary]]; 17 | [self setShortcuts:[NSMutableDictionary dictionary]]; 18 | [self setShortcutMonitor:[MASShortcutMonitor sharedMonitor]]; 19 | [self setBindingOptions:@{NSValueTransformerNameBindingOption: NSKeyedUnarchiveFromDataTransformerName}]; 20 | return self; 21 | } 22 | 23 | - (void) dealloc 24 | { 25 | for (NSString *bindingName in [_actions allKeys]) { 26 | [self unbind:bindingName]; 27 | } 28 | } 29 | 30 | + (instancetype) sharedBinder 31 | { 32 | static dispatch_once_t once; 33 | static MASShortcutBinder *sharedInstance; 34 | dispatch_once(&once, ^{ 35 | sharedInstance = [[self alloc] init]; 36 | }); 37 | return sharedInstance; 38 | } 39 | 40 | #pragma mark Registration 41 | 42 | - (void) bindShortcutWithDefaultsKey: (NSString*) defaultsKeyName toAction: (dispatch_block_t) action 43 | { 44 | NSAssert([defaultsKeyName rangeOfString:@"."].location == NSNotFound, 45 | @"Illegal character in binding name (“.”), please see http://git.io/x5YS."); 46 | NSAssert([defaultsKeyName rangeOfString:@" "].location == NSNotFound, 47 | @"Illegal character in binding name (“ ”), please see http://git.io/x5YS."); 48 | [_actions setObject:[action copy] forKey:defaultsKeyName]; 49 | [self bind:defaultsKeyName 50 | toObject:[NSUserDefaultsController sharedUserDefaultsController] 51 | withKeyPath:[@"values." stringByAppendingString:defaultsKeyName] 52 | options:_bindingOptions]; 53 | } 54 | 55 | - (void) breakBindingWithDefaultsKey: (NSString*) defaultsKeyName 56 | { 57 | [_shortcutMonitor unregisterShortcut:[_shortcuts objectForKey:defaultsKeyName]]; 58 | [_shortcuts removeObjectForKey:defaultsKeyName]; 59 | [_actions removeObjectForKey:defaultsKeyName]; 60 | [self unbind:defaultsKeyName]; 61 | } 62 | 63 | - (void) registerDefaultShortcuts: (NSDictionary*) defaultShortcuts 64 | { 65 | NSValueTransformer *transformer = [_bindingOptions valueForKey:NSValueTransformerBindingOption]; 66 | if (transformer == nil) { 67 | NSString *transformerName = [_bindingOptions valueForKey:NSValueTransformerNameBindingOption]; 68 | if (transformerName) { 69 | transformer = [NSValueTransformer valueTransformerForName:transformerName]; 70 | } 71 | } 72 | 73 | NSAssert(transformer != nil, @"Can’t register default shortcuts without a transformer."); 74 | 75 | [defaultShortcuts enumerateKeysAndObjectsUsingBlock:^(NSString *defaultsKey, MASShortcut *shortcut, BOOL *stop) { 76 | id value = [transformer reverseTransformedValue:shortcut]; 77 | [[NSUserDefaults standardUserDefaults] registerDefaults:@{defaultsKey:value}]; 78 | }]; 79 | } 80 | 81 | #pragma mark Bindings 82 | 83 | - (BOOL) isRegisteredAction: (NSString*) name 84 | { 85 | return !![_actions objectForKey:name]; 86 | } 87 | 88 | - (id) valueForUndefinedKey: (NSString*) key 89 | { 90 | return [self isRegisteredAction:key] ? 91 | [_shortcuts objectForKey:key] : 92 | [super valueForUndefinedKey:key]; 93 | } 94 | 95 | - (void) setValue: (id) value forUndefinedKey: (NSString*) key 96 | { 97 | if (![self isRegisteredAction:key]) { 98 | [super setValue:value forUndefinedKey:key]; 99 | return; 100 | } 101 | 102 | MASShortcut *newShortcut = value; 103 | MASShortcut *currentShortcut = [_shortcuts objectForKey:key]; 104 | 105 | // Unbind previous shortcut if any 106 | if (currentShortcut != nil) { 107 | [_shortcutMonitor unregisterShortcut:currentShortcut]; 108 | } 109 | 110 | // Just deleting the old shortcut 111 | if (newShortcut == nil) { 112 | [_shortcuts removeObjectForKey:key]; 113 | return; 114 | } 115 | 116 | // Bind new shortcut 117 | [_shortcuts setObject:newShortcut forKey:key]; 118 | [_shortcutMonitor registerShortcut:newShortcut withAction:[_actions objectForKey:key]]; 119 | } 120 | 121 | @end 122 | -------------------------------------------------------------------------------- /MASShortcut.xcodeproj/xcshareddata/xcschemes/MASShortcut.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 76 | 77 | 83 | 84 | 85 | 86 | 92 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /Framework/User Defaults Storage/MASShortcutBinderTests.m: -------------------------------------------------------------------------------- 1 | static NSString *const SampleDefaultsKey = @"sampleShortcut"; 2 | 3 | @interface MASShortcutBinderTests : XCTestCase 4 | @property(strong) MASShortcutBinder *binder; 5 | @property(strong) MASShortcutMonitor *monitor; 6 | @property(strong) NSUserDefaults *defaults; 7 | @end 8 | 9 | @implementation MASShortcutBinderTests 10 | 11 | - (void) setUp 12 | { 13 | [super setUp]; 14 | [self setBinder:[[MASShortcutBinder alloc] init]]; 15 | [self setMonitor:[_binder shortcutMonitor]]; 16 | [self setDefaults:[[NSUserDefaults alloc] init]]; 17 | [_defaults removeObjectForKey:SampleDefaultsKey]; 18 | } 19 | 20 | - (void) tearDown 21 | { 22 | [_monitor unregisterAllShortcuts]; 23 | [self setMonitor:nil]; 24 | [self setDefaults:nil]; 25 | [self setBinder:nil]; 26 | [super tearDown]; 27 | } 28 | 29 | - (void) testInitialValueReading 30 | { 31 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; 32 | [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; 33 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 34 | XCTAssertTrue([_monitor isShortcutRegistered:shortcut], 35 | @"Pass the initial shortcut from defaults to shortcut monitor."); 36 | } 37 | 38 | - (void) testValueChangeReading 39 | { 40 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; 41 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 42 | [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; 43 | XCTAssertTrue([_monitor isShortcutRegistered:shortcut], 44 | @"Pass the shortcut from defaults to shortcut monitor after defaults change."); 45 | } 46 | 47 | - (void) testValueClearing 48 | { 49 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; 50 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 51 | [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; 52 | [_defaults removeObjectForKey:SampleDefaultsKey]; 53 | XCTAssertFalse([_monitor isShortcutRegistered:shortcut], 54 | @"Unregister shortcut from monitor after value is cleared from defaults."); 55 | } 56 | 57 | - (void) testBindingRemoval 58 | { 59 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; 60 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 61 | [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; 62 | [_binder breakBindingWithDefaultsKey:SampleDefaultsKey]; 63 | XCTAssertFalse([_monitor isShortcutRegistered:shortcut], 64 | @"Unregister shortcut from monitor after binding was removed."); 65 | } 66 | 67 | - (void) testRebinding 68 | { 69 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:1 modifierFlags:1]; 70 | [_defaults setObject:[NSKeyedArchiver archivedDataWithRootObject:shortcut] forKey:SampleDefaultsKey]; 71 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 72 | [_binder breakBindingWithDefaultsKey:SampleDefaultsKey]; 73 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 74 | XCTAssertTrue([_monitor isShortcutRegistered:shortcut], 75 | @"Bind after unbinding."); 76 | } 77 | 78 | - (void) testTransformerDeserialization 79 | { 80 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576]; 81 | NSDictionary *storedShortcut = @{@"keyCode": @5, @"modifierFlags": @1048576}; 82 | [_defaults setObject:storedShortcut forKey:SampleDefaultsKey]; 83 | [_binder setBindingOptions:@{NSValueTransformerBindingOption:[MASDictionaryTransformer new]}]; 84 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 85 | XCTAssertTrue([_monitor isShortcutRegistered:shortcut], 86 | @"Deserialize shortcut from user defaults using a custom transformer."); 87 | } 88 | 89 | - (void) testDefaultShortcuts 90 | { 91 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:5 modifierFlags:1048576]; 92 | [_binder registerDefaultShortcuts:@{SampleDefaultsKey: shortcut}]; 93 | [_binder bindShortcutWithDefaultsKey:SampleDefaultsKey toAction:^{}]; 94 | XCTAssertTrue([_monitor isShortcutRegistered:shortcut], 95 | @"Bind shortcut using a default value."); 96 | } 97 | 98 | // See issue #64 for rationale and discussion. 99 | - (void) testIllegalSymbolsInBindingNames 100 | { 101 | XCTAssertThrows([_binder bindShortcutWithDefaultsKey:@"foo.bar" toAction:^{}], 102 | @"Throw for illegal binding symbols: a dot (“.”)."); 103 | XCTAssertThrows([_binder bindShortcutWithDefaultsKey:@"foo bar" toAction:^{}], 104 | @"Throw for illegal binding symbols: a space (“ ”)."); 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /Framework/Model/MASShortcutValidator.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutValidator.h" 2 | #import "MASLocalization.h" 3 | 4 | @implementation MASShortcutValidator 5 | 6 | + (instancetype) sharedValidator 7 | { 8 | static dispatch_once_t once; 9 | static MASShortcutValidator *sharedInstance; 10 | dispatch_once(&once, ^{ 11 | sharedInstance = [[self alloc] init]; 12 | }); 13 | return sharedInstance; 14 | } 15 | 16 | - (BOOL) isShortcutValid: (MASShortcut*) shortcut 17 | { 18 | NSInteger keyCode = [shortcut keyCode]; 19 | NSEventModifierFlags modifiers = [shortcut modifierFlags]; 20 | 21 | // Allow any function key with any combination of modifiers 22 | BOOL includesFunctionKey = ((keyCode == kVK_F1) || (keyCode == kVK_F2) || (keyCode == kVK_F3) || (keyCode == kVK_F4) || 23 | (keyCode == kVK_F5) || (keyCode == kVK_F6) || (keyCode == kVK_F7) || (keyCode == kVK_F8) || 24 | (keyCode == kVK_F9) || (keyCode == kVK_F10) || (keyCode == kVK_F11) || (keyCode == kVK_F12) || 25 | (keyCode == kVK_F13) || (keyCode == kVK_F14) || (keyCode == kVK_F15) || (keyCode == kVK_F16) || 26 | (keyCode == kVK_F17) || (keyCode == kVK_F18) || (keyCode == kVK_F19) || (keyCode == kVK_F20)); 27 | if (includesFunctionKey) return YES; 28 | 29 | // Do not allow any other key without modifiers 30 | BOOL hasModifierFlags = (modifiers > 0); 31 | if (!hasModifierFlags) return NO; 32 | 33 | // Allow any hotkey containing Control or Command modifier 34 | BOOL includesCommand = ((modifiers & NSEventModifierFlagCommand) > 0); 35 | BOOL includesControl = ((modifiers & NSEventModifierFlagControl) > 0); 36 | if (includesCommand || includesControl) return YES; 37 | 38 | // Allow Option key only in selected cases 39 | BOOL includesOption = ((modifiers & NSEventModifierFlagOption) > 0); 40 | if (includesOption) { 41 | 42 | // Always allow Option-Space and Option-Escape because they do not have any bind system commands 43 | if ((keyCode == kVK_Space) || (keyCode == kVK_Escape)) return YES; 44 | 45 | // Allow Option modifier with any key even if it will break the system binding 46 | if (_allowAnyShortcutWithOptionModifier) return YES; 47 | } 48 | 49 | // The hotkey does not have any modifiers or violates system bindings 50 | return NO; 51 | } 52 | 53 | - (BOOL) isShortcut: (MASShortcut*) shortcut alreadyTakenInMenu: (NSMenu*) menu explanation: (NSString**) explanation 54 | { 55 | if (self.allowOverridingServicesShortcut && menu == [NSApp servicesMenu]) { 56 | return NO; 57 | } 58 | 59 | NSString *keyEquivalent = [shortcut keyCodeStringForKeyEquivalent]; 60 | NSEventModifierFlags flags = [shortcut modifierFlags]; 61 | 62 | for (NSMenuItem *menuItem in menu.itemArray) { 63 | if (menuItem.hasSubmenu && [self isShortcut:shortcut alreadyTakenInMenu:[menuItem submenu] explanation:explanation]) return YES; 64 | 65 | BOOL equalFlags = (MASPickModifiersIncludingFn(menuItem.keyEquivalentModifierMask) == flags); 66 | BOOL equalHotkeyLowercase = [menuItem.keyEquivalent.lowercaseString isEqualToString:keyEquivalent]; 67 | 68 | // Check if the cases are different, we know ours is lower and that shift is included in our modifiers 69 | // If theirs is capitol, we need to add shift to their modifiers 70 | if (equalHotkeyLowercase && ![menuItem.keyEquivalent isEqualToString:keyEquivalent]) { 71 | equalFlags = (MASPickModifiersIncludingFn(menuItem.keyEquivalentModifierMask | NSEventModifierFlagShift) == flags); 72 | } 73 | 74 | if (equalFlags && equalHotkeyLowercase) { 75 | if (explanation) { 76 | *explanation = MASLocalizedString(@"This shortcut cannot be used because it is already used by the menu item ‘%@’.", 77 | @"Message for alert when shortcut is already used"); 78 | *explanation = [NSString stringWithFormat:*explanation, menuItem.title]; 79 | } 80 | return YES; 81 | } 82 | } 83 | return NO; 84 | } 85 | 86 | - (BOOL) isShortcutAlreadyTakenBySystem: (MASShortcut*) shortcut explanation: (NSString**) explanation 87 | { 88 | CFArrayRef globalHotKeys; 89 | if (CopySymbolicHotKeys(&globalHotKeys) == noErr) { 90 | 91 | // Enumerate all global hotkeys and check if any of them matches current shortcut 92 | for (CFIndex i = 0, count = CFArrayGetCount(globalHotKeys); i < count; i++) { 93 | CFDictionaryRef hotKeyInfo = CFArrayGetValueAtIndex(globalHotKeys, i); 94 | CFNumberRef code = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyCode); 95 | CFNumberRef flags = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyModifiers); 96 | CFNumberRef enabled = CFDictionaryGetValue(hotKeyInfo, kHISymbolicHotKeyEnabled); 97 | 98 | if (([(__bridge NSNumber *)code integerValue] == [shortcut keyCode]) && 99 | ([(__bridge NSNumber *)flags unsignedIntegerValue] == [shortcut carbonFlags]) && 100 | ([(__bridge NSNumber *)enabled boolValue])) { 101 | 102 | if (explanation) { 103 | *explanation = MASLocalizedString(@"This combination cannot be used because it is already used by a system-wide " 104 | @"keyboard shortcut.\nIf you really want to use this key combination, most shortcuts " 105 | @"can be changed in the Keyboard & Mouse panel in System Preferences.", 106 | @"Message for alert when shortcut is already used by the system"); 107 | } 108 | return YES; 109 | } 110 | } 111 | CFRelease(globalHotKeys); 112 | } 113 | return [self isShortcut:shortcut alreadyTakenInMenu:[NSApp mainMenu] explanation:explanation]; 114 | } 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.0) 5 | activesupport (4.2.11.1) 6 | i18n (~> 0.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.3, >= 0.3.4) 9 | tzinfo (~> 1.1) 10 | addressable (2.6.0) 11 | public_suffix (>= 2.0.2, < 4.0) 12 | atomos (0.1.3) 13 | babosa (1.0.2) 14 | claide (1.0.2) 15 | cocoapods (1.7.1) 16 | activesupport (>= 4.0.2, < 5) 17 | claide (>= 1.0.2, < 2.0) 18 | cocoapods-core (= 1.7.1) 19 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 20 | cocoapods-downloader (>= 1.2.2, < 2.0) 21 | cocoapods-plugins (>= 1.0.0, < 2.0) 22 | cocoapods-search (>= 1.0.0, < 2.0) 23 | cocoapods-stats (>= 1.0.0, < 2.0) 24 | cocoapods-trunk (>= 1.3.1, < 2.0) 25 | cocoapods-try (>= 1.1.0, < 2.0) 26 | colored2 (~> 3.1) 27 | escape (~> 0.0.4) 28 | fourflusher (>= 2.2.0, < 3.0) 29 | gh_inspector (~> 1.0) 30 | molinillo (~> 0.6.6) 31 | nap (~> 1.0) 32 | ruby-macho (~> 1.4) 33 | xcodeproj (>= 1.8.2, < 2.0) 34 | cocoapods-core (1.7.1) 35 | activesupport (>= 4.0.2, < 6) 36 | fuzzy_match (~> 2.0.4) 37 | nap (~> 1.0) 38 | cocoapods-deintegrate (1.0.4) 39 | cocoapods-downloader (1.2.2) 40 | cocoapods-plugins (1.0.0) 41 | nap 42 | cocoapods-search (1.0.0) 43 | cocoapods-stats (1.1.0) 44 | cocoapods-trunk (1.3.1) 45 | nap (>= 0.8, < 2.0) 46 | netrc (~> 0.11) 47 | cocoapods-try (1.1.0) 48 | colored (1.2) 49 | colored2 (3.1.2) 50 | commander-fastlane (4.4.6) 51 | highline (~> 1.7.2) 52 | concurrent-ruby (1.1.5) 53 | declarative (0.0.10) 54 | declarative-option (0.1.0) 55 | digest-crc (0.4.1) 56 | domain_name (0.5.20180417) 57 | unf (>= 0.0.5, < 1.0.0) 58 | dotenv (2.7.2) 59 | emoji_regex (1.0.1) 60 | escape (0.0.4) 61 | excon (0.64.0) 62 | faraday (0.15.4) 63 | multipart-post (>= 1.2, < 3) 64 | faraday-cookie_jar (0.0.6) 65 | faraday (>= 0.7.4) 66 | http-cookie (~> 1.0.0) 67 | faraday_middleware (0.13.1) 68 | faraday (>= 0.7.4, < 1.0) 69 | fastimage (2.1.5) 70 | fastlane (2.125.0) 71 | CFPropertyList (>= 2.3, < 4.0.0) 72 | addressable (>= 2.3, < 3.0.0) 73 | babosa (>= 1.0.2, < 2.0.0) 74 | bundler (>= 1.12.0, < 3.0.0) 75 | colored 76 | commander-fastlane (>= 4.4.6, < 5.0.0) 77 | dotenv (>= 2.1.1, < 3.0.0) 78 | emoji_regex (>= 0.1, < 2.0) 79 | excon (>= 0.45.0, < 1.0.0) 80 | faraday (~> 0.9) 81 | faraday-cookie_jar (~> 0.0.6) 82 | faraday_middleware (~> 0.9) 83 | fastimage (>= 2.1.0, < 3.0.0) 84 | gh_inspector (>= 1.1.2, < 2.0.0) 85 | google-api-client (>= 0.21.2, < 0.24.0) 86 | google-cloud-storage (>= 1.15.0, < 2.0.0) 87 | highline (>= 1.7.2, < 2.0.0) 88 | json (< 3.0.0) 89 | jwt (~> 2.1.0) 90 | mini_magick (~> 4.5.1) 91 | multi_xml (~> 0.5) 92 | multipart-post (~> 2.0.0) 93 | plist (>= 3.1.0, < 4.0.0) 94 | public_suffix (~> 2.0.0) 95 | rubyzip (>= 1.2.2, < 2.0.0) 96 | security (= 0.1.3) 97 | simctl (~> 1.6.3) 98 | slack-notifier (>= 2.0.0, < 3.0.0) 99 | terminal-notifier (>= 2.0.0, < 3.0.0) 100 | terminal-table (>= 1.4.5, < 2.0.0) 101 | tty-screen (>= 0.6.3, < 1.0.0) 102 | tty-spinner (>= 0.8.0, < 1.0.0) 103 | word_wrap (~> 1.0.0) 104 | xcodeproj (>= 1.8.1, < 2.0.0) 105 | xcpretty (~> 0.3.0) 106 | xcpretty-travis-formatter (>= 0.0.3) 107 | fastlane-plugin-changelog (0.14.0) 108 | fourflusher (2.2.0) 109 | fuzzy_match (2.0.4) 110 | gh_inspector (1.1.3) 111 | google-api-client (0.23.9) 112 | addressable (~> 2.5, >= 2.5.1) 113 | googleauth (>= 0.5, < 0.7.0) 114 | httpclient (>= 2.8.1, < 3.0) 115 | mime-types (~> 3.0) 116 | representable (~> 3.0) 117 | retriable (>= 2.0, < 4.0) 118 | signet (~> 0.9) 119 | google-cloud-core (1.3.0) 120 | google-cloud-env (~> 1.0) 121 | google-cloud-env (1.0.5) 122 | faraday (~> 0.11) 123 | google-cloud-storage (1.16.0) 124 | digest-crc (~> 0.4) 125 | google-api-client (~> 0.23) 126 | google-cloud-core (~> 1.2) 127 | googleauth (>= 0.6.2, < 0.10.0) 128 | googleauth (0.6.7) 129 | faraday (~> 0.12) 130 | jwt (>= 1.4, < 3.0) 131 | memoist (~> 0.16) 132 | multi_json (~> 1.11) 133 | os (>= 0.9, < 2.0) 134 | signet (~> 0.7) 135 | highline (1.7.10) 136 | http-cookie (1.0.3) 137 | domain_name (~> 0.5) 138 | httpclient (2.8.3) 139 | i18n (0.9.5) 140 | concurrent-ruby (~> 1.0) 141 | json (2.2.0) 142 | jwt (2.1.0) 143 | memoist (0.16.0) 144 | mime-types (3.2.2) 145 | mime-types-data (~> 3.2015) 146 | mime-types-data (3.2019.0331) 147 | mini_magick (4.5.1) 148 | minitest (5.11.3) 149 | molinillo (0.6.6) 150 | multi_json (1.13.1) 151 | multi_xml (0.6.0) 152 | multipart-post (2.0.0) 153 | nanaimo (0.2.6) 154 | nap (1.1.0) 155 | naturally (2.2.0) 156 | netrc (0.11.0) 157 | os (1.0.1) 158 | plist (3.5.0) 159 | public_suffix (2.0.5) 160 | representable (3.0.4) 161 | declarative (< 0.1.0) 162 | declarative-option (< 0.2.0) 163 | uber (< 0.2.0) 164 | retriable (3.1.2) 165 | rouge (2.0.7) 166 | ruby-macho (1.4.0) 167 | rubyzip (1.2.3) 168 | security (0.1.3) 169 | signet (0.11.0) 170 | addressable (~> 2.3) 171 | faraday (~> 0.9) 172 | jwt (>= 1.5, < 3.0) 173 | multi_json (~> 1.10) 174 | simctl (1.6.5) 175 | CFPropertyList 176 | naturally 177 | slack-notifier (2.3.2) 178 | terminal-notifier (2.0.0) 179 | terminal-table (1.8.0) 180 | unicode-display_width (~> 1.1, >= 1.1.1) 181 | thread_safe (0.3.6) 182 | tty-cursor (0.7.0) 183 | tty-screen (0.7.0) 184 | tty-spinner (0.9.1) 185 | tty-cursor (~> 0.7) 186 | tzinfo (1.2.5) 187 | thread_safe (~> 0.1) 188 | uber (0.1.0) 189 | unf (0.1.4) 190 | unf_ext 191 | unf_ext (0.0.7.6) 192 | unicode-display_width (1.6.0) 193 | word_wrap (1.0.0) 194 | xcodeproj (1.9.0) 195 | CFPropertyList (>= 2.3.3, < 4.0) 196 | atomos (~> 0.1.3) 197 | claide (>= 1.0.2, < 2.0) 198 | colored2 (~> 3.1) 199 | nanaimo (~> 0.2.6) 200 | xcpretty (0.3.0) 201 | rouge (~> 2.0.7) 202 | xcpretty-travis-formatter (1.0.0) 203 | xcpretty (~> 0.2, >= 0.0.7) 204 | 205 | PLATFORMS 206 | ruby 207 | 208 | DEPENDENCIES 209 | cocoapods 210 | fastlane 211 | fastlane-plugin-changelog 212 | 213 | BUNDLED WITH 214 | 1.16.2 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/shpakovski/MASShortcut.svg?branch=master)](https://travis-ci.org/shpakovski/MASShortcut) 2 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 3 | 4 | # Intro 5 | 6 | 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. 7 | 8 | The MASShortcut project introduces a modern API and user interface for recording, storing and using system-wide keyboard shortcuts. 9 | 10 | ![Screenshot of the demo project](https://raw.githubusercontent.com/shpakovski/MASShortcut/master/Demo/screenshot.png "This is how the demo looks like") 11 | 12 | Features: 13 | 14 | * Record and display keyboard shortcuts 15 | * Watch for shortcuts and execute actions, system-wide 16 | * A nice, [documented API](http://cocoadocs.org/docsets/MASShortcut/) 17 | * Can be configured to be compatible with Shortcut Recorder 18 | * Can be installed both through CocoaPods and as a Git submodule 19 | * Mac App Store friendly 20 | * Works on OS X 10.10 and up 21 | * Hacking-friendly codebase covered with tests 22 | 23 | Partially done: 24 | 25 | * Accessibility support. There’s some basic accessibility code, testers and feedback welcome. 26 | * Localisation. The English and Czech localization should be complete, there’s basic support for German, French, Spanish, Italian, and Japanese. If you’re a native speaker in one of the mentioned languages, please test the localization and report issues or add missing strings. 27 | 28 | Pull requests welcome :) 29 | 30 | # Installation 31 | ### Swift Package Manager 32 | [Swift Package Manager](https://swift.org/package-manager/) is the simplest way to install for Xcode projects. Simply add the following Package Dependency: 33 | 34 | https://github.com/shpakovski/MASShortcut 35 | 36 | 37 | ### CocoaPods 38 | You can also use [CocoaPods](http://cocoapods.org/), by adding the following line to your Podfile: 39 | 40 | pod 'MASShortcut' 41 | 42 | If you want to stick to the 1.x branch, you can use the version smart match operator: 43 | 44 | pod 'MASShortcut', '~> 1' 45 | 46 | ### Carthage 47 | You can also install via [Carthage](https://github.com/Carthage/Carthage), or you can use Git submodules and link against the MASShortcut framework manually. 48 | 49 | To build from the command line, type 'make release'. The framework will be created in a temporary directory and revealed in Finder when the build is complete. 50 | 51 | # Usage 52 | 53 | I hope, it is really easy: 54 | 55 | ```objective-c 56 | #import 57 | 58 | // Drop a custom view into XIB, set its class to MASShortcutView 59 | // and its height to 19. If you select another appearance style, 60 | // look up the correct height values in MASShortcutView.h. 61 | @property (nonatomic, weak) IBOutlet MASShortcutView *shortcutView; 62 | 63 | // Pick a preference key to store the shortcut between launches 64 | static NSString *const kPreferenceGlobalShortcut = @"GlobalShortcut"; 65 | 66 | // Associate the shortcut view with user defaults 67 | self.shortcutView.associatedUserDefaultsKey = kPreferenceGlobalShortcut; 68 | 69 | // Associate the preference key with an action 70 | [[MASShortcutBinder sharedBinder] 71 | bindShortcutWithDefaultsKey:kPreferenceGlobalShortcut 72 | toAction:^{ 73 | // Let me know if you find a better or a more convenient API. 74 | }]; 75 | ``` 76 | 77 | When you have installed via a method other than Swift Package Manager, then the import is slightly different: 78 | 79 | ```objective-c 80 | #import 81 | ``` 82 | 83 | You can see a real usage example in the Demo target. Enjoy! 84 | 85 | # Shortcut Recorder Compatibility 86 | 87 | By default, MASShortcut uses a different User Defaults storage format incompatible with Shortcut Recorder. But it’s easily possible to change that, so that you can replace Shortcut Recorder with MASShortcut without having to migrate the shortcuts previously stored by your apps. There are two parts of the story: 88 | 89 | If you bind the recorder control (`MASShortcutView`) to User defaults, set the Value Transformer field in the Interface Builder to `MASDictionaryTransformer`. This makes sure the shortcuts are written in the Shortcut Recorder format. 90 | 91 | If you use `MASShortcutBinder` to automatically load shortcuts from User Defaults, set the `bindingOptions` accordingly: 92 | 93 | ```objective-c 94 | [[MASShortcutBinder sharedBinder] setBindingOptions:@{NSValueTransformerNameBindingOption:MASDictionaryTransformerName}]; 95 | ``` 96 | 97 | This makes sure that the shortcuts in the Shortcut Recorder format are loaded correctly. 98 | 99 | # Notifications 100 | 101 | 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. 102 | 103 | This is just as easy to implement: 104 | 105 | ```objective-c 106 | // Declare an ivar for key path in the user defaults controller 107 | NSString *_observableKeyPath; 108 | 109 | // Make a global context reference 110 | void *kGlobalShortcutContext = &kGlobalShortcutContext; 111 | 112 | // Implement when loading view 113 | _observableKeyPath = [@"values." stringByAppendingString:kPreferenceGlobalShortcut]; 114 | [[NSUserDefaultsController sharedUserDefaultsController] addObserver:self forKeyPath:_observableKeyPath 115 | options:NSKeyValueObservingOptionInitial 116 | context:kGlobalShortcutContext]; 117 | 118 | // Capture the KVO change and do something 119 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)obj 120 | change:(NSDictionary *)change context:(void *)ctx 121 | { 122 | if (ctx == kGlobalShortcutContext) { 123 | NSLog(@"Shortcut has changed"); 124 | } 125 | else { 126 | [super observeValueForKeyPath:keyPath ofObject:obj change:change context:ctx]; 127 | } 128 | } 129 | 130 | // Do not forget to remove the observer 131 | [[NSUserDefaultsController sharedUserDefaultsController] removeObserver:self 132 | forKeyPath:_observableKeyPath 133 | context:kGlobalShortcutContext]; 134 | ``` 135 | 136 | # Using in Swift projects 137 | 138 | Swift Package Manager is the simplest way to import MASShortcut, just import the Module like so: 139 | 140 | ``` 141 | import MASShortcut 142 | ``` 143 | 144 | Alternatively, you can also: 145 | 146 | 1. Install as a Pod using the latest CocoaPods with Swift support. 147 | 2. Create a bridging header file [using the instructions here](http://swiftalicio.us/2014/11/using-cocoapods-from-swift/) 148 | 3. Your bridging header file should contain the following [two](https://github.com/shpakovski/MASShortcut/issues/36) imports: 149 | 150 | ```objective-c 151 | #import 152 | #import 153 | ``` 154 | 155 | # Copyright 156 | 157 | MASShortcut is licensed under the 2-clause BSD license. 158 | -------------------------------------------------------------------------------- /Demo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | static NSString *const MASCustomShortcutKey = @"customShortcut"; 4 | static NSString *const MASCustomShortcutEnabledKey = @"customShortcutEnabled"; 5 | static NSString *const MASHardcodedShortcutEnabledKey = @"hardcodedShortcutEnabled"; 6 | 7 | static void *MASObservingContext = &MASObservingContext; 8 | 9 | @interface AppDelegate () 10 | @property(strong) IBOutlet MASShortcutView *customShortcutView; 11 | @property(strong) IBOutlet NSTextField *feedbackTextField; 12 | @property(strong) IBOutlet NSVisualEffectView *visualEffectView; 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | - (void) awakeFromNib 18 | { 19 | [super awakeFromNib]; 20 | 21 | NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; 22 | 23 | // Most apps need default shortcut, delete these lines if this is not your case 24 | MASShortcut *firstLaunchShortcut = [MASShortcut shortcutWithKeyCode:kVK_F1 modifierFlags:NSEventModifierFlagCommand]; 25 | NSData *firstLaunchShortcutData = [NSKeyedArchiver archivedDataWithRootObject:firstLaunchShortcut]; 26 | 27 | // Register default values to be used for the first app start 28 | [defaults registerDefaults:@{ 29 | MASHardcodedShortcutEnabledKey : @YES, 30 | MASCustomShortcutEnabledKey : @YES, 31 | MASCustomShortcutKey : firstLaunchShortcutData 32 | }]; 33 | 34 | // Bind the shortcut recorder view’s value to user defaults. 35 | // Run “defaults read com.shpakovski.mac.Demo” to see what’s stored 36 | // in user defaults. 37 | [_customShortcutView setAssociatedUserDefaultsKey:MASCustomShortcutKey]; 38 | 39 | // Enable or disable the recorder view according to the first checkbox state 40 | [_customShortcutView bind:@"enabled" toObject:defaults 41 | withKeyPath:MASCustomShortcutEnabledKey options:nil]; 42 | 43 | // Watch user defaults for changes in the checkbox states 44 | [defaults addObserver:self forKeyPath:MASCustomShortcutEnabledKey 45 | options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew 46 | context:MASObservingContext]; 47 | [defaults addObserver:self forKeyPath:MASHardcodedShortcutEnabledKey 48 | options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew 49 | context:MASObservingContext]; 50 | } 51 | 52 | - (void)playShortcutFeedback 53 | { 54 | [[NSSound soundNamed:@"Ping"] play]; 55 | [self.feedbackTextField setStringValue:NSLocalizedString(@"Shortcut pressed!", @"Feedback that’s displayed when user presses the sample shortcut.")]; 56 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 57 | [self.feedbackTextField setStringValue:@""]; 58 | }); 59 | } 60 | 61 | // Handle changes in user defaults. We have to check keyPath here to see which of the 62 | // two checkboxes was changed. This is not very elegant, in practice you could use something 63 | // like https://github.com/facebook/KVOController with a nicer API. 64 | - (void) observeValueForKeyPath: (NSString*) keyPath ofObject: (id) object change: (NSDictionary*) change context: (void*) context 65 | { 66 | if (context != MASObservingContext) { 67 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 68 | return; 69 | } 70 | 71 | BOOL newValue = [[change objectForKey:NSKeyValueChangeNewKey] boolValue]; 72 | if ([keyPath isEqualToString:MASCustomShortcutEnabledKey]) { 73 | [self setCustomShortcutEnabled:newValue]; 74 | } else if ([keyPath isEqualToString:MASHardcodedShortcutEnabledKey]) { 75 | [self setHardcodedShortcutEnabled:newValue]; 76 | } 77 | } 78 | 79 | - (void) setCustomShortcutEnabled: (BOOL) enabled 80 | { 81 | if (enabled) { 82 | [[MASShortcutBinder sharedBinder] bindShortcutWithDefaultsKey:MASCustomShortcutKey toAction:^{ 83 | [self playShortcutFeedback]; 84 | }]; 85 | } else { 86 | [[MASShortcutBinder sharedBinder] breakBindingWithDefaultsKey:MASCustomShortcutKey]; 87 | } 88 | } 89 | 90 | - (void) setHardcodedShortcutEnabled: (BOOL) enabled 91 | { 92 | MASShortcut *shortcut = [MASShortcut shortcutWithKeyCode:kVK_F2 modifierFlags:NSEventModifierFlagCommand]; 93 | if (enabled) { 94 | [[MASShortcutMonitor sharedMonitor] registerShortcut:shortcut withAction:^{ 95 | [self playShortcutFeedback]; 96 | }]; 97 | } else { 98 | [[MASShortcutMonitor sharedMonitor] unregisterShortcut:shortcut]; 99 | } 100 | } 101 | 102 | #pragma mark Actions 103 | 104 | // These actions let you configure the NSVisualEffect view and test the MASShortcutView in a variety of scenarios. 105 | 106 | - (void) displayMojaveAlertWithMessage: (NSString *) message 107 | { 108 | NSAlert *alert = [NSAlert new]; 109 | alert.messageText = @"Not Available"; 110 | alert.informativeText = [NSString stringWithFormat:@"The %@ is only available on Mojave (10.14) and later", message]; 111 | [alert addButtonWithTitle:@"OK"]; 112 | [alert runModal]; 113 | } 114 | 115 | - (IBAction) setAppearance: (id) sender 116 | { 117 | if ([sender isKindOfClass:[NSPopUpButton class]]) { 118 | NSPopUpButton *popUpButton = (NSPopUpButton *)sender; 119 | 120 | NSInteger tag = popUpButton.selectedTag; 121 | switch (tag) { 122 | case 0: // Inherited 123 | self.visualEffectView.appearance = nil; 124 | break; 125 | case 1: // Vibrant Light 126 | self.visualEffectView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]; 127 | break; 128 | case 2: // Vibrant Dark 129 | self.visualEffectView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameVibrantDark]; 130 | break; 131 | case 3: // Aqua 132 | self.visualEffectView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; 133 | break; 134 | case 4: // Dark Aqua 135 | if (@available(macOS 10.14, *)) { 136 | self.visualEffectView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameDarkAqua]; 137 | } else { 138 | [self displayMojaveAlertWithMessage:@"Dark Aqua Appearance"]; 139 | } 140 | break; 141 | 142 | default: 143 | break; 144 | } 145 | } 146 | } 147 | 148 | - (IBAction) setMaterial: (id) sender 149 | { 150 | if ([sender isKindOfClass:[NSPopUpButton class]]) { 151 | NSPopUpButton *popUpButton = (NSPopUpButton *)sender; 152 | 153 | NSInteger tag = popUpButton.selectedTag; 154 | switch (tag) { 155 | case 0: // Popover 156 | self.visualEffectView.material = NSVisualEffectMaterialPopover; 157 | break; 158 | case 1: // Sidebar 159 | self.visualEffectView.material = NSVisualEffectMaterialSidebar; 160 | break; 161 | case 2: // HUD Window 162 | if (@available(macOS 10.14, *)) { 163 | self.visualEffectView.material = NSVisualEffectMaterialHUDWindow; 164 | } else { 165 | [self displayMojaveAlertWithMessage:@"HUD Window Material"]; 166 | } 167 | break; 168 | case 3: // Window Background 169 | if (@available(macOS 10.14, *)) { 170 | self.visualEffectView.material = NSVisualEffectMaterialWindowBackground; 171 | } else { 172 | [self displayMojaveAlertWithMessage:@"Window Background Material"]; 173 | } 174 | break; 175 | 176 | default: 177 | break; 178 | } 179 | } 180 | } 181 | 182 | #pragma mark NSApplicationDelegate 183 | 184 | - (BOOL) applicationShouldTerminateAfterLastWindowClosed: (NSApplication*) sender 185 | { 186 | return YES; 187 | } 188 | 189 | @end 190 | -------------------------------------------------------------------------------- /Framework/Model/MASShortcut.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcut.h" 2 | #import "MASLocalization.h" 3 | 4 | static NSString *const MASShortcutKeyCode = @"KeyCode"; 5 | static NSString *const MASShortcutModifierFlags = @"ModifierFlags"; 6 | 7 | @implementation MASShortcut 8 | 9 | #pragma mark Initialization 10 | 11 | - (instancetype)initWithKeyCode:(NSInteger)code modifierFlags:(NSEventModifierFlags)flags 12 | { 13 | self = [super init]; 14 | if (self) { 15 | _keyCode = code; 16 | _modifierFlags = MASPickCocoaModifiers(flags); 17 | } 18 | return self; 19 | } 20 | 21 | + (instancetype)shortcutWithKeyCode:(NSInteger)code modifierFlags:(NSEventModifierFlags)flags 22 | { 23 | return [[self alloc] initWithKeyCode:code modifierFlags:flags]; 24 | } 25 | 26 | + (instancetype)shortcutWithEvent:(NSEvent *)event 27 | { 28 | return [[self alloc] initWithKeyCode:event.keyCode modifierFlags:event.modifierFlags]; 29 | } 30 | 31 | #pragma mark Shortcut Accessors 32 | 33 | - (UInt32)carbonKeyCode 34 | { 35 | return (self.keyCode == NSNotFound ? 0 : (UInt32)self.keyCode); 36 | } 37 | 38 | - (UInt32)carbonFlags 39 | { 40 | return MASCarbonModifiersFromCocoaModifiers(self.modifierFlags); 41 | } 42 | 43 | - (NSString *)description 44 | { 45 | return [NSString stringWithFormat:@"%@%@", self.modifierFlagsString, self.keyCodeString]; 46 | } 47 | 48 | - (NSString *)keyCodeStringForKeyEquivalent 49 | { 50 | NSString *keyCodeString = self.keyCodeString; 51 | 52 | switch (self.keyCode) { 53 | case kVK_F1: return NSStringFromMASKeyCode(NSF1FunctionKey); 54 | case kVK_F2: return NSStringFromMASKeyCode(NSF2FunctionKey); 55 | case kVK_F3: return NSStringFromMASKeyCode(NSF3FunctionKey); 56 | case kVK_F4: return NSStringFromMASKeyCode(NSF4FunctionKey); 57 | case kVK_F5: return NSStringFromMASKeyCode(NSF5FunctionKey); 58 | case kVK_F6: return NSStringFromMASKeyCode(NSF6FunctionKey); 59 | case kVK_F7: return NSStringFromMASKeyCode(NSF7FunctionKey); 60 | case kVK_F8: return NSStringFromMASKeyCode(NSF8FunctionKey); 61 | case kVK_F9: return NSStringFromMASKeyCode(NSF9FunctionKey); 62 | case kVK_F10: return NSStringFromMASKeyCode(NSF10FunctionKey); 63 | case kVK_F11: return NSStringFromMASKeyCode(NSF11FunctionKey); 64 | case kVK_F12: return NSStringFromMASKeyCode(NSF12FunctionKey); 65 | case kVK_F13: return NSStringFromMASKeyCode(NSF13FunctionKey); 66 | case kVK_F14: return NSStringFromMASKeyCode(NSF14FunctionKey); 67 | case kVK_F15: return NSStringFromMASKeyCode(NSF15FunctionKey); 68 | case kVK_F16: return NSStringFromMASKeyCode(NSF16FunctionKey); 69 | case kVK_F17: return NSStringFromMASKeyCode(NSF17FunctionKey); 70 | case kVK_F18: return NSStringFromMASKeyCode(NSF18FunctionKey); 71 | case kVK_F19: return NSStringFromMASKeyCode(NSF19FunctionKey); 72 | case kVK_Space: return NSStringFromMASKeyCode(kMASShortcutSpaceFunctionKey); 73 | case kVK_Escape: return NSStringFromMASKeyCode(kMASShortcutEscapeFunctionKey); 74 | case kVK_Delete: return NSStringFromMASKeyCode(NSBackspaceCharacter); 75 | case kVK_ForwardDelete: return NSStringFromMASKeyCode(NSDeleteFunctionKey); 76 | case kVK_LeftArrow: return NSStringFromMASKeyCode(NSLeftArrowFunctionKey); 77 | case kVK_RightArrow: return NSStringFromMASKeyCode(NSRightArrowFunctionKey); 78 | case kVK_UpArrow: return NSStringFromMASKeyCode(NSUpArrowFunctionKey); 79 | case kVK_DownArrow: return NSStringFromMASKeyCode(NSDownArrowFunctionKey); 80 | case kVK_Help: return NSStringFromMASKeyCode(NSHelpFunctionKey); 81 | case kVK_Home: return NSStringFromMASKeyCode(NSHomeFunctionKey); 82 | case kVK_End: return NSStringFromMASKeyCode(NSEndFunctionKey); 83 | case kVK_PageUp: return NSStringFromMASKeyCode(NSPageUpFunctionKey); 84 | case kVK_PageDown: return NSStringFromMASKeyCode(NSPageDownFunctionKey); 85 | case kVK_Tab: return NSStringFromMASKeyCode(kMASShortcutTabFunctionKey); 86 | case kVK_Return: return NSStringFromMASKeyCode(kMASShortcutReturnFunctionKey); 87 | } 88 | 89 | return keyCodeString.lowercaseString; 90 | } 91 | 92 | - (NSString *)keyCodeString 93 | { 94 | // Some key codes don't have an equivalent 95 | switch (self.keyCode) { 96 | case NSNotFound: return @""; 97 | case kVK_F1: return @"F1"; 98 | case kVK_F2: return @"F2"; 99 | case kVK_F3: return @"F3"; 100 | case kVK_F4: return @"F4"; 101 | case kVK_F5: return @"F5"; 102 | case kVK_F6: return @"F6"; 103 | case kVK_F7: return @"F7"; 104 | case kVK_F8: return @"F8"; 105 | case kVK_F9: return @"F9"; 106 | case kVK_F10: return @"F10"; 107 | case kVK_F11: return @"F11"; 108 | case kVK_F12: return @"F12"; 109 | case kVK_F13: return @"F13"; 110 | case kVK_F14: return @"F14"; 111 | case kVK_F15: return @"F15"; 112 | case kVK_F16: return @"F16"; 113 | case kVK_F17: return @"F17"; 114 | case kVK_F18: return @"F18"; 115 | case kVK_F19: return @"F19"; 116 | case kVK_Space: return MASLocalizedString(@"Space", @"Shortcut glyph name for SPACE key"); 117 | case kVK_Escape: return NSStringFromMASKeyCode(kMASShortcutGlyphEscape); 118 | case kVK_Delete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteLeft); 119 | case kVK_ForwardDelete: return NSStringFromMASKeyCode(kMASShortcutGlyphDeleteRight); 120 | case kVK_LeftArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphLeftArrow); 121 | case kVK_RightArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphRightArrow); 122 | case kVK_UpArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphUpArrow); 123 | case kVK_DownArrow: return NSStringFromMASKeyCode(kMASShortcutGlyphDownArrow); 124 | case kVK_Help: return NSStringFromMASKeyCode(kMASShortcutGlyphHelp); // Insert 125 | case kVK_Home: return NSStringFromMASKeyCode(kMASShortcutGlyphNorthwestArrow); 126 | case kVK_End: return NSStringFromMASKeyCode(kMASShortcutGlyphSoutheastArrow); 127 | case kVK_PageUp: return NSStringFromMASKeyCode(kMASShortcutGlyphPageUp); 128 | case kVK_PageDown: return NSStringFromMASKeyCode(kMASShortcutGlyphPageDown); 129 | case kVK_Tab: return NSStringFromMASKeyCode(kMASShortcutGlyphTabRight); 130 | case kVK_Return: return NSStringFromMASKeyCode(kMASShortcutGlyphReturnR2L); 131 | 132 | // Keypad 133 | case kVK_ANSI_Keypad0: return @"0"; 134 | case kVK_ANSI_Keypad1: return @"1"; 135 | case kVK_ANSI_Keypad2: return @"2"; 136 | case kVK_ANSI_Keypad3: return @"3"; 137 | case kVK_ANSI_Keypad4: return @"4"; 138 | case kVK_ANSI_Keypad5: return @"5"; 139 | case kVK_ANSI_Keypad6: return @"6"; 140 | case kVK_ANSI_Keypad7: return @"7"; 141 | case kVK_ANSI_Keypad8: return @"8"; 142 | case kVK_ANSI_Keypad9: return @"9"; 143 | case kVK_ANSI_KeypadDecimal: return @"."; 144 | case kVK_ANSI_KeypadMultiply: return @"*"; 145 | case kVK_ANSI_KeypadPlus: return @"+"; 146 | case kVK_ANSI_KeypadClear: return NSStringFromMASKeyCode(kMASShortcutGlyphPadClear); 147 | case kVK_ANSI_KeypadDivide: return @"/"; 148 | case kVK_ANSI_KeypadEnter: return NSStringFromMASKeyCode(kMASShortcutGlyphReturn); 149 | case kVK_ANSI_KeypadMinus: return @"-"; 150 | case kVK_ANSI_KeypadEquals: return @"="; 151 | } 152 | 153 | // Everything else should be printable so look it up in the current ASCII capable keyboard layout 154 | OSStatus error = noErr; 155 | NSString *keystroke = nil; 156 | TISInputSourceRef inputSource = TISCopyCurrentASCIICapableKeyboardLayoutInputSource(); 157 | if (inputSource) { 158 | CFDataRef layoutDataRef = TISGetInputSourceProperty(inputSource, kTISPropertyUnicodeKeyLayoutData); 159 | if (layoutDataRef) { 160 | UCKeyboardLayout *layoutData = (UCKeyboardLayout *)CFDataGetBytePtr(layoutDataRef); 161 | UniCharCount length = 0; 162 | UniChar chars[256] = { 0 }; 163 | UInt32 deadKeyState = 0; 164 | error = UCKeyTranslate(layoutData, (UInt16)self.keyCode, kUCKeyActionDisplay, 0, // No modifiers 165 | LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &deadKeyState, 166 | sizeof(chars) / sizeof(UniChar), &length, chars); 167 | keystroke = ((error == noErr) && length ? [NSString stringWithCharacters:chars length:length] : @""); 168 | } 169 | CFRelease(inputSource); 170 | } 171 | 172 | // Validate keystroke 173 | if (keystroke.length) { 174 | static NSMutableCharacterSet *validChars = nil; 175 | if (validChars == nil) { 176 | validChars = [[NSMutableCharacterSet alloc] init]; 177 | [validChars formUnionWithCharacterSet:[NSCharacterSet alphanumericCharacterSet]]; 178 | [validChars formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]]; 179 | [validChars formUnionWithCharacterSet:[NSCharacterSet symbolCharacterSet]]; 180 | } 181 | for (NSUInteger i = 0, length = keystroke.length; i < length; i++) { 182 | if (![validChars characterIsMember:[keystroke characterAtIndex:i]]) { 183 | keystroke = @""; 184 | break; 185 | } 186 | } 187 | } 188 | 189 | // Finally, we've got a shortcut! 190 | return keystroke.uppercaseString; 191 | } 192 | 193 | - (NSString *)modifierFlagsString 194 | { 195 | unichar chars[4]; 196 | NSUInteger count = 0; 197 | // These are in the same order as the menu manager shows them 198 | if (self.modifierFlags & NSEventModifierFlagControl) chars[count++] = kControlUnicode; 199 | if (self.modifierFlags & NSEventModifierFlagOption) chars[count++] = kOptionUnicode; 200 | if (self.modifierFlags & NSEventModifierFlagShift) chars[count++] = kShiftUnicode; 201 | if (self.modifierFlags & NSEventModifierFlagCommand) chars[count++] = kCommandUnicode; 202 | return (count ? [NSString stringWithCharacters:chars length:count] : @""); 203 | } 204 | 205 | #pragma mark NSObject 206 | 207 | - (BOOL) isEqual: (MASShortcut*) object 208 | { 209 | return [object isKindOfClass:[self class]] 210 | && (object.keyCode == self.keyCode) 211 | && (object.modifierFlags == self.modifierFlags); 212 | } 213 | 214 | - (NSUInteger) hash 215 | { 216 | return self.keyCode + self.modifierFlags; 217 | } 218 | 219 | #pragma mark NSCoding 220 | 221 | - (void)encodeWithCoder:(NSCoder *)coder 222 | { 223 | [coder encodeInteger:(self.keyCode != NSNotFound ? self.keyCode : - 1) forKey:MASShortcutKeyCode]; 224 | [coder encodeInteger:(NSInteger)self.modifierFlags forKey:MASShortcutModifierFlags]; 225 | } 226 | 227 | - (instancetype)initWithCoder:(NSCoder *)decoder 228 | { 229 | self = [super init]; 230 | if (self) { 231 | NSInteger code = [decoder decodeIntegerForKey:MASShortcutKeyCode]; 232 | _keyCode = (code < 0) ? NSNotFound : code; 233 | _modifierFlags = [decoder decodeIntegerForKey:MASShortcutModifierFlags]; 234 | } 235 | return self; 236 | } 237 | 238 | #pragma mark NSSecureCoding 239 | 240 | + (BOOL)supportsSecureCoding 241 | { 242 | return YES; 243 | } 244 | 245 | #pragma mark NSCopying 246 | 247 | - (instancetype) copyWithZone:(NSZone *)zone 248 | { 249 | return [[self class] shortcutWithKeyCode:_keyCode modifierFlags:_modifierFlags]; 250 | } 251 | 252 | @end 253 | -------------------------------------------------------------------------------- /Framework/UI/MASShortcutView.m: -------------------------------------------------------------------------------- 1 | #import "MASShortcutView.h" 2 | #import "MASShortcutValidator.h" 3 | #import "MASLocalization.h" 4 | #import "MASShortcutViewButtonCell.h" 5 | 6 | NSString *const MASShortcutBinding = @"shortcutValue"; 7 | 8 | static const CGFloat MASHintButtonWidth = 23; 9 | static const CGFloat MASButtonFontSize = 11; 10 | 11 | #pragma mark - 12 | 13 | @interface MASShortcutView () // Private accessors 14 | 15 | @property (nonatomic, getter = isHinting) BOOL hinting; 16 | @property (nonatomic, copy) NSString *shortcutPlaceholder; 17 | @property (nonatomic, assign) BOOL showsDeleteButton; 18 | 19 | @end 20 | 21 | #pragma mark - 22 | 23 | @implementation MASShortcutView { 24 | MASShortcutViewButtonCell *_shortcutCell; 25 | NSInteger _shortcutToolTipTag; 26 | NSInteger _hintToolTipTag; 27 | NSTrackingArea *_hintArea; 28 | BOOL _acceptsFirstResponder; 29 | } 30 | 31 | #pragma mark - 32 | 33 | + (Class)shortcutCellClass 34 | { 35 | return [MASShortcutViewButtonCell class]; 36 | } 37 | 38 | - (id)initWithFrame:(CGRect)frameRect 39 | { 40 | self = [super initWithFrame:frameRect]; 41 | if (self) { 42 | [self commonInit]; 43 | } 44 | return self; 45 | } 46 | 47 | - (id)initWithCoder:(NSCoder *)coder 48 | { 49 | self = [super initWithCoder:coder]; 50 | if (self) { 51 | [self commonInit]; 52 | } 53 | return self; 54 | } 55 | 56 | - (void)commonInit 57 | { 58 | _shortcutCell = [[[self.class shortcutCellClass] alloc] init]; 59 | _shortcutCell.buttonType = NSButtonTypePushOnPushOff; 60 | _shortcutCell.font = [[NSFontManager sharedFontManager] convertFont:_shortcutCell.font toSize:MASButtonFontSize]; 61 | _shortcutValidator = [MASShortcutValidator sharedValidator]; 62 | _enabled = YES; 63 | _showsDeleteButton = YES; 64 | _acceptsFirstResponder = NO; 65 | [self resetShortcutCellStyle]; 66 | } 67 | 68 | - (void)dealloc 69 | { 70 | [self activateEventMonitoring:NO]; 71 | [self activateResignObserver:NO]; 72 | } 73 | 74 | #pragma mark - Public accessors 75 | 76 | - (void)setEnabled:(BOOL)flag 77 | { 78 | if (_enabled != flag) { 79 | _enabled = flag; 80 | [self updateTrackingAreas]; 81 | self.recording = NO; 82 | [self invalidateIntrinsicContentSize]; 83 | [self setNeedsDisplay:YES]; 84 | } 85 | } 86 | 87 | - (void)setStyle:(MASShortcutViewStyle)newStyle 88 | { 89 | if (_style != newStyle) { 90 | _style = newStyle; 91 | [self resetShortcutCellStyle]; 92 | [self invalidateIntrinsicContentSize]; 93 | [self setNeedsDisplay:YES]; 94 | } 95 | } 96 | 97 | - (void)resetShortcutCellStyle 98 | { 99 | switch (_style) { 100 | case MASShortcutViewStyleDefault: { 101 | _shortcutCell.bezelStyle = NSBezelStyleRoundRect; 102 | break; 103 | } 104 | case MASShortcutViewStyleTexturedRect: { 105 | _shortcutCell.bezelStyle = NSBezelStyleTexturedRounded; 106 | break; 107 | } 108 | case MASShortcutViewStyleRounded: { 109 | _shortcutCell.bezelStyle = NSBezelStyleRounded; 110 | break; 111 | } 112 | case MASShortcutViewStyleFlat: { 113 | self.wantsLayer = YES; 114 | _shortcutCell.backgroundColor = [NSColor clearColor]; 115 | _shortcutCell.bordered = NO; 116 | break; 117 | } 118 | case MASShortcutViewStyleRegularSquare: { 119 | _shortcutCell.bezelStyle = NSBezelStyleRegularSquare; 120 | break; 121 | } 122 | } 123 | } 124 | 125 | - (void)setRecording:(BOOL)flag 126 | { 127 | // Only one recorder can be active at the moment 128 | static MASShortcutView *currentRecorder = nil; 129 | if (flag && (currentRecorder != self)) { 130 | currentRecorder.recording = NO; 131 | currentRecorder = flag ? self : nil; 132 | } 133 | 134 | // Only enabled view supports recording 135 | if (flag && !self.enabled) return; 136 | 137 | // Only care about changes in state 138 | if (flag == _recording) return; 139 | 140 | _recording = flag; 141 | self.shortcutPlaceholder = nil; 142 | [self resetToolTips]; 143 | [self activateEventMonitoring:_recording]; 144 | [self activateResignObserver:_recording]; 145 | [self invalidateIntrinsicContentSize]; 146 | [self setNeedsDisplay:YES]; 147 | 148 | // Give VoiceOver users feedback on the result. Requires at least 10.9 to run. 149 | // We’re silencing the “tautological compare” warning here so that if someone 150 | // takes the naked source files and compiles them with -Wall, the following 151 | // NSAccessibilityPriorityKey comparison doesn’t cause a warning. See: 152 | // https://github.com/shpakovski/MASShortcut/issues/76 153 | #pragma clang diagnostic push 154 | #pragma clang diagnostic ignored "-Wtautological-compare" 155 | if (_recording == NO && (&NSAccessibilityPriorityKey != NULL)) { 156 | NSString* msg = _shortcutValue ? 157 | MASLocalizedString(@"Shortcut set", @"VoiceOver: Shortcut set") : 158 | MASLocalizedString(@"Shortcut cleared", @"VoiceOver: Shortcut cleared"); 159 | NSDictionary *announcementInfo = @{ 160 | NSAccessibilityAnnouncementKey : msg, 161 | NSAccessibilityPriorityKey : @(NSAccessibilityPriorityHigh), 162 | }; 163 | NSAccessibilityPostNotificationWithUserInfo(self, NSAccessibilityAnnouncementRequestedNotification, announcementInfo); 164 | } 165 | #pragma clang diagnostic pop 166 | } 167 | 168 | - (void)setShortcutValue:(MASShortcut *)shortcutValue 169 | { 170 | _shortcutValue = shortcutValue; 171 | [self resetToolTips]; 172 | [self invalidateIntrinsicContentSize]; 173 | [self setNeedsDisplay:YES]; 174 | [self propagateValue:shortcutValue forBinding:MASShortcutBinding]; 175 | 176 | if (self.shortcutValueChange) { 177 | self.shortcutValueChange(self); 178 | } 179 | } 180 | 181 | - (void)setShortcutPlaceholder:(NSString *)shortcutPlaceholder 182 | { 183 | _shortcutPlaceholder = shortcutPlaceholder.copy; 184 | [self invalidateIntrinsicContentSize]; 185 | [self setNeedsDisplay:YES]; 186 | } 187 | 188 | #pragma mark - Appearance 189 | 190 | - (BOOL)allowsVibrancy 191 | { 192 | return YES; 193 | } 194 | 195 | #pragma mark - Drawing 196 | 197 | - (BOOL)isFlipped 198 | { 199 | return YES; 200 | } 201 | 202 | - (void)drawInRect:(CGRect)frame withTitle:(NSString *)title alignment:(NSTextAlignment)alignment state:(NSInteger)state 203 | { 204 | _shortcutCell.title = title; 205 | _shortcutCell.alignment = alignment; 206 | _shortcutCell.state = state; 207 | _shortcutCell.enabled = self.enabled; 208 | 209 | CGFloat yOffset; 210 | 211 | switch (_style) { 212 | case MASShortcutViewStyleTexturedRect: 213 | case MASShortcutViewStyleRounded: 214 | case MASShortcutViewStyleRegularSquare: { 215 | yOffset = 1.0; 216 | break; 217 | } 218 | default: 219 | yOffset = 0.0; 220 | break; 221 | } 222 | 223 | [_shortcutCell drawWithFrame:CGRectOffset(frame, 0.0, yOffset) inView:self]; 224 | } 225 | 226 | - (void)drawRect:(CGRect)dirtyRect 227 | { 228 | if (self.shortcutValue) { 229 | NSString *buttonTitle; 230 | if (self.recording) { 231 | buttonTitle = NSStringFromMASKeyCode(kMASShortcutGlyphEscape); 232 | } else if (self.showsDeleteButton) { 233 | buttonTitle = NSStringFromMASKeyCode(kMASShortcutGlyphClear); 234 | } 235 | if (buttonTitle != nil) { 236 | [self drawInRect:self.bounds withTitle:buttonTitle alignment:NSTextAlignmentRight state:NSControlStateValueOff]; 237 | } 238 | CGRect shortcutRect; 239 | [self getShortcutRect:&shortcutRect hintRect:NULL]; 240 | NSString *title = (self.recording 241 | ? (_hinting 242 | ? MASLocalizedString(@"Use Old Shortcut", @"Cancel action button for non-empty shortcut in recording state") 243 | : (self.shortcutPlaceholder.length > 0 244 | ? self.shortcutPlaceholder 245 | : MASLocalizedString(@"Type New Shortcut", @"Non-empty shortcut button in recording state"))) 246 | : _shortcutValue ? _shortcutValue.description : @""); 247 | [self drawInRect:shortcutRect withTitle:title alignment:NSTextAlignmentCenter state:self.isRecording ? NSControlStateValueOn : NSControlStateValueOff]; 248 | } 249 | else { 250 | if (self.recording) 251 | { 252 | [self drawInRect:self.bounds withTitle:NSStringFromMASKeyCode(kMASShortcutGlyphEscape) alignment:NSTextAlignmentRight state:NSControlStateValueOff]; 253 | 254 | CGRect shortcutRect; 255 | [self getShortcutRect:&shortcutRect hintRect:NULL]; 256 | NSString *title = (_hinting 257 | ? MASLocalizedString(@"Cancel", @"Cancel action button in recording state") 258 | : (self.shortcutPlaceholder.length > 0 259 | ? self.shortcutPlaceholder 260 | : MASLocalizedString(@"Type Shortcut", @"Empty shortcut button in recording state"))); 261 | [self drawInRect:shortcutRect withTitle:title alignment:NSTextAlignmentCenter state:NSControlStateValueOn]; 262 | } 263 | else 264 | { 265 | [self drawInRect:self.bounds withTitle:MASLocalizedString(@"Record Shortcut", @"Empty shortcut button in normal state") 266 | alignment:NSTextAlignmentCenter state:NSControlStateValueOff]; 267 | } 268 | } 269 | } 270 | 271 | 272 | - (NSSize)intrinsicContentSize 273 | { 274 | NSSize cellSize = _shortcutCell.cellSize; 275 | 276 | // Use a "fake" value for width. Since determining the actual width requires information 277 | // that is not determined until drawRect: is called, it doesn't seem feasible to properly 278 | // calculate the intrinsic size without refactoring the code. That would give better results, 279 | // however. 280 | 281 | // 120 is an arbitrary number that seems to be wide enough for English localization. This 282 | // may need to be adjusted for other locales/languages. 283 | 284 | // NOTE: Simply returning cellSize results in a display that is sometimes correct 285 | // and sometimes not, and changes based on whether the mouse is hovering or not. 286 | return NSMakeSize(120, cellSize.height); 287 | } 288 | 289 | 290 | #pragma mark - Mouse handling 291 | 292 | - (void)getShortcutRect:(CGRect *)shortcutRectRef hintRect:(CGRect *)hintRectRef 293 | { 294 | CGRect shortcutRect, hintRect; 295 | CGFloat hintButtonWidth = MASHintButtonWidth; 296 | switch (self.style) { 297 | case MASShortcutViewStyleTexturedRect: hintButtonWidth += 2.0; break; 298 | case MASShortcutViewStyleRounded: hintButtonWidth += 3.0; break; 299 | case MASShortcutViewStyleFlat: hintButtonWidth -= 8.0 - (_shortcutCell.font.pointSize - MASButtonFontSize); break; 300 | default: break; 301 | } 302 | CGRectDivide(self.bounds, &hintRect, &shortcutRect, hintButtonWidth, CGRectMaxXEdge); 303 | if (shortcutRectRef) *shortcutRectRef = shortcutRect; 304 | if (hintRectRef) *hintRectRef = hintRect; 305 | } 306 | 307 | - (BOOL)locationInShortcutRect:(CGPoint)location 308 | { 309 | CGRect shortcutRect; 310 | [self getShortcutRect:&shortcutRect hintRect:NULL]; 311 | return CGRectContainsPoint(shortcutRect, [self convertPoint:location fromView:nil]); 312 | } 313 | 314 | - (BOOL)locationInHintRect:(CGPoint)location 315 | { 316 | CGRect hintRect; 317 | [self getShortcutRect:NULL hintRect:&hintRect]; 318 | return CGRectContainsPoint(hintRect, [self convertPoint:location fromView:nil]); 319 | } 320 | 321 | - (void)mouseDown:(NSEvent *)event 322 | { 323 | if (self.enabled) { 324 | if (self.shortcutValue) { 325 | if (self.recording) { 326 | if ([self locationInHintRect:event.locationInWindow]) { 327 | self.recording = NO; 328 | } 329 | } 330 | else { 331 | if ([self locationInShortcutRect:event.locationInWindow]) { 332 | self.recording = YES; 333 | } 334 | else { 335 | self.shortcutValue = nil; 336 | } 337 | } 338 | } 339 | else { 340 | if (self.recording) { 341 | if ([self locationInHintRect:event.locationInWindow]) { 342 | self.recording = NO; 343 | } 344 | } 345 | else { 346 | self.recording = YES; 347 | } 348 | } 349 | } 350 | else { 351 | [super mouseDown:event]; 352 | } 353 | } 354 | 355 | #pragma mark - Handling mouse over 356 | 357 | - (void)updateTrackingAreas 358 | { 359 | [super updateTrackingAreas]; 360 | 361 | if (_hintArea) { 362 | [self removeTrackingArea:_hintArea]; 363 | _hintArea = nil; 364 | } 365 | 366 | // Forbid hinting if view is disabled 367 | if (!self.enabled) return; 368 | 369 | CGRect hintRect; 370 | [self getShortcutRect:NULL hintRect:&hintRect]; 371 | NSTrackingAreaOptions options = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingAssumeInside); 372 | _hintArea = [[NSTrackingArea alloc] initWithRect:hintRect options:options owner:self userInfo:nil]; 373 | [self addTrackingArea:_hintArea]; 374 | } 375 | 376 | - (void)setHinting:(BOOL)flag 377 | { 378 | if (_hinting != flag) { 379 | _hinting = flag; 380 | [self invalidateIntrinsicContentSize]; 381 | [self setNeedsDisplay:YES]; 382 | } 383 | } 384 | 385 | - (void)mouseEntered:(NSEvent *)event 386 | { 387 | self.hinting = YES; 388 | } 389 | 390 | - (void)mouseExited:(NSEvent *)event 391 | { 392 | self.hinting = NO; 393 | } 394 | 395 | void *kUserDataShortcut = &kUserDataShortcut; 396 | void *kUserDataHint = &kUserDataHint; 397 | 398 | - (void)resetToolTips 399 | { 400 | if (_shortcutToolTipTag) { 401 | [self removeToolTip:_shortcutToolTipTag]; 402 | _shortcutToolTipTag = 0; 403 | } 404 | if (_hintToolTipTag) { 405 | [self removeToolTip:_hintToolTipTag]; 406 | _hintToolTipTag = 0; 407 | } 408 | 409 | if ((self.shortcutValue == nil) || self.recording || !self.enabled) return; 410 | 411 | CGRect shortcutRect, hintRect; 412 | [self getShortcutRect:&shortcutRect hintRect:&hintRect]; 413 | _shortcutToolTipTag = [self addToolTipRect:shortcutRect owner:self userData:kUserDataShortcut]; 414 | _hintToolTipTag = [self addToolTipRect:hintRect owner:self userData:kUserDataHint]; 415 | } 416 | 417 | - (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(CGPoint)point userData:(void *)data 418 | { 419 | if (data == kUserDataShortcut) { 420 | return MASLocalizedString(@"Click to record new shortcut", @"Tooltip for non-empty shortcut button"); 421 | } 422 | else if (data == kUserDataHint) { 423 | return MASLocalizedString(@"Delete shortcut", @"Tooltip for hint button near the non-empty shortcut"); 424 | } 425 | return @""; 426 | } 427 | 428 | #pragma mark - Event monitoring 429 | 430 | - (void)activateEventMonitoring:(BOOL)shouldActivate 431 | { 432 | static BOOL isActive = NO; 433 | if (isActive == shouldActivate) return; 434 | isActive = shouldActivate; 435 | 436 | static id eventMonitor = nil; 437 | if (shouldActivate) { 438 | __unsafe_unretained MASShortcutView *weakSelf = self; 439 | NSEventMask eventMask = (NSEventMaskKeyDown | NSEventMaskFlagsChanged); 440 | eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:eventMask handler:^(NSEvent *event) { 441 | 442 | // Create a shortcut from the event 443 | MASShortcut *shortcut = [MASShortcut shortcutWithEvent:event]; 444 | 445 | // Tab key must pass through. 446 | if (shortcut.keyCode == kVK_Tab){ 447 | return event; 448 | } 449 | 450 | // If the shortcut is a plain Delete or Backspace, clear the current shortcut and cancel recording 451 | if (!shortcut.modifierFlags && ((shortcut.keyCode == kVK_Delete) || (shortcut.keyCode == kVK_ForwardDelete))) { 452 | weakSelf.shortcutValue = nil; 453 | weakSelf.recording = NO; 454 | event = nil; 455 | } 456 | 457 | // If the shortcut is a plain Esc, cancel recording 458 | else if (!shortcut.modifierFlags && shortcut.keyCode == kVK_Escape) { 459 | weakSelf.recording = NO; 460 | event = nil; 461 | } 462 | 463 | // If the shortcut is Cmd-W or Cmd-Q, cancel recording and pass the event through 464 | else if ((shortcut.modifierFlags == NSEventModifierFlagCommand) && (shortcut.keyCode == kVK_ANSI_W || shortcut.keyCode == kVK_ANSI_Q)) { 465 | weakSelf.recording = NO; 466 | } 467 | 468 | else { 469 | // Verify possible shortcut 470 | if (shortcut.keyCodeString.length > 0) { 471 | if (!weakSelf.shortcutValidator || [weakSelf.shortcutValidator isShortcutValid:shortcut]) { 472 | // Verify that shortcut is not used 473 | NSString *explanation = nil; 474 | if ([weakSelf.shortcutValidator isShortcutAlreadyTakenBySystem:shortcut explanation:&explanation]) { 475 | // Prevent cancel of recording when Alert window is key 476 | [weakSelf activateResignObserver:NO]; 477 | [weakSelf activateEventMonitoring:NO]; 478 | NSString *format = MASLocalizedString(@"The key combination %@ cannot be used", 479 | @"Title for alert when shortcut is already used"); 480 | NSAlert* alert = [[NSAlert alloc]init]; 481 | alert.alertStyle = NSAlertStyleCritical; 482 | alert.informativeText = explanation; 483 | alert.messageText = [NSString stringWithFormat:format, shortcut]; 484 | [alert addButtonWithTitle:MASLocalizedString(@"OK", @"Alert button when shortcut is already used")]; 485 | 486 | [alert runModal]; 487 | weakSelf.shortcutPlaceholder = nil; 488 | [weakSelf activateResignObserver:YES]; 489 | [weakSelf activateEventMonitoring:YES]; 490 | } 491 | else { 492 | weakSelf.shortcutValue = shortcut; 493 | weakSelf.recording = NO; 494 | } 495 | } 496 | else { 497 | // Key press with or without SHIFT is not valid input 498 | NSBeep(); 499 | } 500 | } 501 | else { 502 | // User is playing with modifier keys 503 | weakSelf.shortcutPlaceholder = shortcut.modifierFlagsString; 504 | } 505 | event = nil; 506 | } 507 | return event; 508 | }]; 509 | } 510 | else { 511 | [NSEvent removeMonitor:eventMonitor]; 512 | } 513 | } 514 | 515 | - (void)activateResignObserver:(BOOL)shouldActivate 516 | { 517 | static BOOL isActive = NO; 518 | if (isActive == shouldActivate) return; 519 | isActive = shouldActivate; 520 | 521 | static id observer = nil; 522 | NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; 523 | if (shouldActivate) { 524 | __unsafe_unretained MASShortcutView *weakSelf = self; 525 | observer = [notificationCenter addObserverForName:NSWindowDidResignKeyNotification object:self.window 526 | queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) { 527 | weakSelf.recording = NO; 528 | }]; 529 | } 530 | else { 531 | [notificationCenter removeObserver:observer]; 532 | } 533 | } 534 | 535 | #pragma mark Bindings 536 | 537 | // http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/ 538 | -(void) propagateValue:(id)value forBinding:(NSString*)binding 539 | { 540 | NSParameterAssert(binding != nil); 541 | 542 | //WARNING: bindingInfo contains NSNull, so it must be accounted for 543 | NSDictionary* bindingInfo = [self infoForBinding:binding]; 544 | if(!bindingInfo) 545 | return; //there is no binding 546 | 547 | //apply the value transformer, if one has been set 548 | NSDictionary* bindingOptions = [bindingInfo objectForKey:NSOptionsKey]; 549 | if(bindingOptions){ 550 | NSValueTransformer* transformer = [bindingOptions valueForKey:NSValueTransformerBindingOption]; 551 | if(!transformer || (id)transformer == [NSNull null]){ 552 | NSString* transformerName = [bindingOptions valueForKey:NSValueTransformerNameBindingOption]; 553 | if(transformerName && (id)transformerName != [NSNull null]){ 554 | transformer = [NSValueTransformer valueTransformerForName:transformerName]; 555 | } 556 | } 557 | 558 | if(transformer && (id)transformer != [NSNull null]){ 559 | if([[transformer class] allowsReverseTransformation]){ 560 | value = [transformer reverseTransformedValue:value]; 561 | } else { 562 | NSLog(@"WARNING: binding \"%@\" has value transformer, but it doesn't allow reverse transformations in %s", binding, __PRETTY_FUNCTION__); 563 | } 564 | } 565 | } 566 | 567 | id boundObject = [bindingInfo objectForKey:NSObservedObjectKey]; 568 | if(!boundObject || boundObject == [NSNull null]){ 569 | NSLog(@"ERROR: NSObservedObjectKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__); 570 | return; 571 | } 572 | 573 | NSString* boundKeyPath = [bindingInfo objectForKey:NSObservedKeyPathKey]; 574 | if(!boundKeyPath || (id)boundKeyPath == [NSNull null]){ 575 | NSLog(@"ERROR: NSObservedKeyPathKey was nil for binding \"%@\" in %s", binding, __PRETTY_FUNCTION__); 576 | return; 577 | } 578 | 579 | [boundObject setValue:value forKeyPath:boundKeyPath]; 580 | } 581 | 582 | #pragma mark - Accessibility 583 | 584 | - (BOOL)isAccessibilityElement 585 | { 586 | return YES; 587 | } 588 | 589 | - (NSString *)accessibilityHelp 590 | { 591 | return MASLocalizedString(@"To record a new shortcut, click this button, and then type the" 592 | @" new shortcut, or press delete to clear an existing shortcut.", 593 | @"VoiceOver shortcut help"); 594 | } 595 | 596 | - (NSString *)accessibilityLabel 597 | { 598 | NSString* title = _shortcutValue.description ?: @"Empty"; 599 | title = [title stringByAppendingFormat:@" %@", MASLocalizedString(@"keyboard shortcut", @"VoiceOver title")]; 600 | return title; 601 | } 602 | 603 | - (BOOL)accessibilityPerformPress 604 | { 605 | if (self.isRecording == NO) { 606 | self.recording = YES; 607 | return YES; 608 | } 609 | else { 610 | return NO; 611 | } 612 | } 613 | 614 | - (NSString *)accessibilityRole 615 | { 616 | return NSAccessibilityButtonRole; 617 | } 618 | 619 | - (BOOL)acceptsFirstResponder 620 | { 621 | return _acceptsFirstResponder; 622 | } 623 | 624 | - (void)setAcceptsFirstResponder:(BOOL)value 625 | { 626 | _acceptsFirstResponder = value; 627 | } 628 | 629 | - (BOOL)becomeFirstResponder 630 | { 631 | [self invalidateIntrinsicContentSize]; 632 | [self setNeedsDisplay:YES]; 633 | return [super becomeFirstResponder]; 634 | } 635 | 636 | - (BOOL)resignFirstResponder 637 | { 638 | [self invalidateIntrinsicContentSize]; 639 | [self setNeedsDisplay:YES]; 640 | return [super resignFirstResponder]; 641 | } 642 | 643 | - (void)drawFocusRingMask 644 | { 645 | [_shortcutCell drawFocusRingMaskWithFrame:[self bounds] inView:self]; 646 | } 647 | 648 | - (NSRect)focusRingMaskBounds 649 | { 650 | return [self bounds]; 651 | } 652 | 653 | @end 654 | -------------------------------------------------------------------------------- /MASShortcut.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 0D2CAB131B8332E5005431FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0D2CAB151B8332E5005431FC /* Localizable.strings */; }; 11 | 0D2CAB191B8339F4005431FC /* MASLocalization.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D2CAB171B8339F4005431FC /* MASLocalization.h */; }; 12 | 0D2CAB1A1B8339F4005431FC /* MASLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D2CAB181B8339F4005431FC /* MASLocalization.m */; }; 13 | 0D2CAB1B1B83409C005431FC /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0D2CAB1D1B83409C005431FC /* MainMenu.xib */; }; 14 | 0D2CAB211B834464005431FC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0D2CAB231B834464005431FC /* Localizable.strings */; }; 15 | 0D39DCA21A668A4400639145 /* MASHotKeyTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D39DCA11A668A4400639145 /* MASHotKeyTests.m */; }; 16 | 0D39DCA41A668E5500639145 /* MASShortcutMonitorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D39DCA31A668E5500639145 /* MASShortcutMonitorTests.m */; }; 17 | 0D827CD71990D4420010B8EF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD61990D4420010B8EF /* Cocoa.framework */; }; 18 | 0D827D251990D55E0010B8EF /* MASShortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D1B1990D55E0010B8EF /* MASShortcut.h */; settings = {ATTRIBUTES = (Public, ); }; }; 19 | 0D827D261990D55E0010B8EF /* MASShortcut.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D1C1990D55E0010B8EF /* MASShortcut.m */; }; 20 | 0D827D2B1990D55E0010B8EF /* MASShortcutView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D211990D55E0010B8EF /* MASShortcutView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 21 | 0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D221990D55E0010B8EF /* MASShortcutView.m */; }; 22 | 0D827D381990D5E70010B8EF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD61990D4420010B8EF /* Cocoa.framework */; }; 23 | 0D827D6F1990D6110010B8EF /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D6A1990D6110010B8EF /* AppDelegate.m */; }; 24 | 0D827D711990D6110010B8EF /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D6D1990D6110010B8EF /* main.m */; }; 25 | 0D827D731990D6590010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; }; 26 | 0D827D751990D6A60010B8EF /* MASShortcut.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 27 | 0D827D771990F81E0010B8EF /* Shortcut.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D761990F81E0010B8EF /* Shortcut.h */; settings = {ATTRIBUTES = (Public, ); }; }; 28 | 0D827D9419910B740010B8EF /* MASShortcutTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9319910B740010B8EF /* MASShortcutTests.m */; }; 29 | 0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; }; 30 | 0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9619910FF70010B8EF /* MASKeyCodes.h */; settings = {ATTRIBUTES = (Public, ); }; }; 31 | 0D827D99199110F60010B8EF /* Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D98199110F60010B8EF /* Prefix.pch */; }; 32 | 0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 33 | 0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */; }; 34 | 0D827DA519912D240010B8EF /* MASShortcutMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35 | 0D827DAD199132840010B8EF /* MASShortcutBinder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D827DAB199132840010B8EF /* MASShortcutBinder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 36 | 0DC2F17619922798003A0131 /* MASHotKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F17419922798003A0131 /* MASHotKey.h */; }; 37 | 0DC2F17719922798003A0131 /* MASHotKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F17519922798003A0131 /* MASHotKey.m */; }; 38 | 0DC2F17C199232EA003A0131 /* MASShortcutMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */; }; 39 | 0DC2F17D199232F7003A0131 /* MASShortcutBinder.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D827DAC199132840010B8EF /* MASShortcutBinder.m */; }; 40 | 0DC2F18919925F8F003A0131 /* MASShortcutBinderTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */; }; 41 | 0DC2F18D1993708A003A0131 /* MASDictionaryTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 42 | 0DC2F18E1993708A003A0131 /* MASDictionaryTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */; }; 43 | 0DC2F190199372B4003A0131 /* MASDictionaryTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */; }; 44 | 0DC2F19819938EFA003A0131 /* MASShortcutView+Bindings.h in Headers */ = {isa = PBXBuildFile; fileRef = 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */; settings = {ATTRIBUTES = (Public, ); }; }; 45 | 0DC2F19919938EFA003A0131 /* MASShortcutView+Bindings.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */; }; 46 | 50C888F126F8E2FE0086EB9A /* MASShortcutViewButtonCell.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C888EF26F8E2FE0086EB9A /* MASShortcutViewButtonCell.h */; settings = {ATTRIBUTES = (Public, ); }; }; 47 | 50C888F226F8E2FE0086EB9A /* MASShortcutViewButtonCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 50C888F026F8E2FE0086EB9A /* MASShortcutViewButtonCell.m */; }; 48 | /* End PBXBuildFile section */ 49 | 50 | /* Begin PBXContainerItemProxy section */ 51 | 0D827D8E19910AFF0010B8EF /* PBXContainerItemProxy */ = { 52 | isa = PBXContainerItemProxy; 53 | containerPortal = 0D827CCA1990D4420010B8EF /* Project object */; 54 | proxyType = 1; 55 | remoteGlobalIDString = 0D827CD21990D4420010B8EF; 56 | remoteInfo = MASShortcut; 57 | }; 58 | /* End PBXContainerItemProxy section */ 59 | 60 | /* Begin PBXCopyFilesBuildPhase section */ 61 | 0D827D741990D6980010B8EF /* Copy Frameworks */ = { 62 | isa = PBXCopyFilesBuildPhase; 63 | buildActionMask = 2147483647; 64 | dstPath = ""; 65 | dstSubfolderSpec = 10; 66 | files = ( 67 | 0D827D751990D6A60010B8EF /* MASShortcut.framework in Copy Frameworks */, 68 | ); 69 | name = "Copy Frameworks"; 70 | runOnlyForDeploymentPostprocessing = 0; 71 | }; 72 | /* End PBXCopyFilesBuildPhase section */ 73 | 74 | /* Begin PBXFileReference section */ 75 | 0D2CAB141B8332E5005431FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 76 | 0D2CAB161B8332EE005431FC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 77 | 0D2CAB171B8339F4005431FC /* MASLocalization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASLocalization.h; sourceTree = ""; }; 78 | 0D2CAB181B8339F4005431FC /* MASLocalization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASLocalization.m; sourceTree = ""; }; 79 | 0D2CAB1E1B8340A4005431FC /* cs */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = cs; path = cs.lproj/MainMenu.xib; sourceTree = ""; }; 80 | 0D2CAB221B834464005431FC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 81 | 0D2CAB241B834467005431FC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 82 | 0D39DCA11A668A4400639145 /* MASHotKeyTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASHotKeyTests.m; sourceTree = ""; }; 83 | 0D39DCA31A668E5500639145 /* MASShortcutMonitorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutMonitorTests.m; sourceTree = ""; }; 84 | 0D58DE521BA165FC0023BFBE /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 85 | 0D58DE531BA166170023BFBE /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; 86 | 0D58DE541BA166270023BFBE /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; 87 | 0D58DE551BA166390023BFBE /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; 88 | 0D58DE561BA166420023BFBE /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 89 | 0D827CD31990D4420010B8EF /* MASShortcut.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MASShortcut.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 90 | 0D827CD61990D4420010B8EF /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 91 | 0D827CD91990D4420010B8EF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 92 | 0D827CDA1990D4420010B8EF /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 93 | 0D827CDB1990D4420010B8EF /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 94 | 0D827D1B1990D55E0010B8EF /* MASShortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASShortcut.h; sourceTree = ""; }; 95 | 0D827D1C1990D55E0010B8EF /* MASShortcut.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcut.m; sourceTree = ""; }; 96 | 0D827D211990D55E0010B8EF /* MASShortcutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASShortcutView.h; sourceTree = ""; }; 97 | 0D827D221990D55E0010B8EF /* MASShortcutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutView.m; sourceTree = ""; }; 98 | 0D827D2F1990D5640010B8EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 99 | 0D827D371990D5E70010B8EF /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 100 | 0D827D691990D6110010B8EF /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 101 | 0D827D6A1990D6110010B8EF /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 102 | 0D827D6B1990D6110010B8EF /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 103 | 0D827D6C1990D6110010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = ""; }; 104 | 0D827D6D1990D6110010B8EF /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 105 | 0D827D761990F81E0010B8EF /* Shortcut.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Shortcut.h; sourceTree = ""; }; 106 | 0D827D8319910AFF0010B8EF /* MASShortcutTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MASShortcutTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 107 | 0D827D8719910AFF0010B8EF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 108 | 0D827D8D19910AFF0010B8EF /* Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = ""; }; 109 | 0D827D9319910B740010B8EF /* MASShortcutTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutTests.m; sourceTree = ""; }; 110 | 0D827D9619910FF70010B8EF /* MASKeyCodes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASKeyCodes.h; sourceTree = ""; }; 111 | 0D827D98199110F60010B8EF /* Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Prefix.pch; sourceTree = ""; }; 112 | 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASShortcutValidator.h; sourceTree = ""; }; 113 | 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutValidator.m; sourceTree = ""; }; 114 | 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASShortcutMonitor.h; sourceTree = ""; }; 115 | 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutMonitor.m; sourceTree = ""; }; 116 | 0D827DAB199132840010B8EF /* MASShortcutBinder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASShortcutBinder.h; sourceTree = ""; }; 117 | 0D827DAC199132840010B8EF /* MASShortcutBinder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutBinder.m; sourceTree = ""; }; 118 | 0DC2F17419922798003A0131 /* MASHotKey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASHotKey.h; sourceTree = ""; }; 119 | 0DC2F17519922798003A0131 /* MASHotKey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASHotKey.m; sourceTree = ""; }; 120 | 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutBinderTests.m; sourceTree = ""; }; 121 | 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASDictionaryTransformer.h; sourceTree = ""; }; 122 | 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASDictionaryTransformer.m; sourceTree = ""; }; 123 | 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASDictionaryTransformerTests.m; sourceTree = ""; }; 124 | 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MASShortcutView+Bindings.h"; sourceTree = ""; }; 125 | 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MASShortcutView+Bindings.m"; sourceTree = ""; }; 126 | 0DEDAA021C6BB479001605F5 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; 127 | 50C888EF26F8E2FE0086EB9A /* MASShortcutViewButtonCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MASShortcutViewButtonCell.h; sourceTree = ""; }; 128 | 50C888F026F8E2FE0086EB9A /* MASShortcutViewButtonCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MASShortcutViewButtonCell.m; sourceTree = ""; }; 129 | 57B25C2D1E78E06D0061A9EC /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 130 | 57B25C2E1E78E06D0061A9EC /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; 131 | 6EA6034E1CBF822000A3ED9C /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; 132 | 6EA6034F1CBF822800A3ED9C /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; 133 | 6EA603501CBF822D00A3ED9C /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; 134 | 6EA603511CBF823600A3ED9C /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 135 | 76A0597D1C51DC940014B271 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 136 | 76A0597E1C51DC9F0014B271 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 137 | EAFFDC811AACFF3300F38834 /* MASShortcut.modulemap */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = "sourcecode.module-map"; path = MASShortcut.modulemap; sourceTree = ""; }; 138 | ED840EAE25E66B37003F76F7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 139 | ED8737791BCE459800BB1716 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; 140 | FDFF016F20CB2FB400CC88F3 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; 141 | /* End PBXFileReference section */ 142 | 143 | /* Begin PBXFrameworksBuildPhase section */ 144 | 0D827CCF1990D4420010B8EF /* Frameworks */ = { 145 | isa = PBXFrameworksBuildPhase; 146 | buildActionMask = 2147483647; 147 | files = ( 148 | 0D827CD71990D4420010B8EF /* Cocoa.framework in Frameworks */, 149 | ); 150 | runOnlyForDeploymentPostprocessing = 0; 151 | }; 152 | 0D827D341990D5E70010B8EF /* Frameworks */ = { 153 | isa = PBXFrameworksBuildPhase; 154 | buildActionMask = 2147483647; 155 | files = ( 156 | 0D827D381990D5E70010B8EF /* Cocoa.framework in Frameworks */, 157 | 0D827D731990D6590010B8EF /* MASShortcut.framework in Frameworks */, 158 | ); 159 | runOnlyForDeploymentPostprocessing = 0; 160 | }; 161 | 0D827D8019910AFF0010B8EF /* Frameworks */ = { 162 | isa = PBXFrameworksBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | 0D827D9519910C1E0010B8EF /* MASShortcut.framework in Frameworks */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXFrameworksBuildPhase section */ 170 | 171 | /* Begin PBXGroup section */ 172 | 0D827CC91990D4420010B8EF = { 173 | isa = PBXGroup; 174 | children = ( 175 | 0DBA0B9E22A4FAC5008685CD /* Framework */, 176 | 0D827D8519910AFF0010B8EF /* Test Support */, 177 | 0DBA0B9C22A4F88C008685CD /* Resources */, 178 | 0D827D681990D6110010B8EF /* Demo */, 179 | 0D827CD51990D4420010B8EF /* Frameworks */, 180 | 0D827CD41990D4420010B8EF /* Products */, 181 | ); 182 | sourceTree = ""; 183 | }; 184 | 0D827CD41990D4420010B8EF /* Products */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 0D827CD31990D4420010B8EF /* MASShortcut.framework */, 188 | 0D827D371990D5E70010B8EF /* Demo.app */, 189 | 0D827D8319910AFF0010B8EF /* MASShortcutTests.xctest */, 190 | ); 191 | name = Products; 192 | sourceTree = ""; 193 | }; 194 | 0D827CD51990D4420010B8EF /* Frameworks */ = { 195 | isa = PBXGroup; 196 | children = ( 197 | 0D827CD61990D4420010B8EF /* Cocoa.framework */, 198 | 0D827CD91990D4420010B8EF /* Foundation.framework */, 199 | 0D827CDA1990D4420010B8EF /* CoreData.framework */, 200 | 0D827CDB1990D4420010B8EF /* AppKit.framework */, 201 | ); 202 | name = Frameworks; 203 | sourceTree = ""; 204 | }; 205 | 0D827D681990D6110010B8EF /* Demo */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 0D2CAB231B834464005431FC /* Localizable.strings */, 209 | 0D827D691990D6110010B8EF /* AppDelegate.h */, 210 | 0D827D6A1990D6110010B8EF /* AppDelegate.m */, 211 | 0D2CAB1D1B83409C005431FC /* MainMenu.xib */, 212 | 0D827D6B1990D6110010B8EF /* Info.plist */, 213 | 0D827D6C1990D6110010B8EF /* Prefix.pch */, 214 | 0D827D6D1990D6110010B8EF /* main.m */, 215 | ); 216 | path = Demo; 217 | sourceTree = ""; 218 | }; 219 | 0D827D8519910AFF0010B8EF /* Test Support */ = { 220 | isa = PBXGroup; 221 | children = ( 222 | 0D827D8719910AFF0010B8EF /* Info.plist */, 223 | 0D827D8D19910AFF0010B8EF /* Prefix.pch */, 224 | ); 225 | name = "Test Support"; 226 | path = Tests; 227 | sourceTree = ""; 228 | }; 229 | 0DBA0B9C22A4F88C008685CD /* Resources */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | 0D2CAB151B8332E5005431FC /* Localizable.strings */, 233 | ); 234 | name = Resources; 235 | path = Framework/Resources; 236 | sourceTree = ""; 237 | }; 238 | 0DBA0B9E22A4FAC5008685CD /* Framework */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 0DBA0B9F22A4FACE008685CD /* Model */, 242 | 0DBA0BA022A4FAE0008685CD /* Monitoring */, 243 | 0DBA0BA122A4FAF6008685CD /* User Defaults Storage */, 244 | 0DBA0BA222A4FB15008685CD /* UI */, 245 | 0D827D2F1990D5640010B8EF /* Info.plist */, 246 | EAFFDC811AACFF3300F38834 /* MASShortcut.modulemap */, 247 | 0D827D98199110F60010B8EF /* Prefix.pch */, 248 | 0D827D761990F81E0010B8EF /* Shortcut.h */, 249 | ); 250 | path = Framework; 251 | sourceTree = ""; 252 | }; 253 | 0DBA0B9F22A4FACE008685CD /* Model */ = { 254 | isa = PBXGroup; 255 | children = ( 256 | 0D827D9619910FF70010B8EF /* MASKeyCodes.h */, 257 | 0D827D1B1990D55E0010B8EF /* MASShortcut.h */, 258 | 0D827D1C1990D55E0010B8EF /* MASShortcut.m */, 259 | 0D827D9319910B740010B8EF /* MASShortcutTests.m */, 260 | 0D827D9C19911A190010B8EF /* MASShortcutValidator.h */, 261 | 0D827D9D19911A190010B8EF /* MASShortcutValidator.m */, 262 | ); 263 | path = Model; 264 | sourceTree = ""; 265 | }; 266 | 0DBA0BA022A4FAE0008685CD /* Monitoring */ = { 267 | isa = PBXGroup; 268 | children = ( 269 | 0DC2F17419922798003A0131 /* MASHotKey.h */, 270 | 0DC2F17519922798003A0131 /* MASHotKey.m */, 271 | 0D39DCA11A668A4400639145 /* MASHotKeyTests.m */, 272 | 0D827DA319912D240010B8EF /* MASShortcutMonitor.h */, 273 | 0D827DA419912D240010B8EF /* MASShortcutMonitor.m */, 274 | 0D39DCA31A668E5500639145 /* MASShortcutMonitorTests.m */, 275 | ); 276 | path = Monitoring; 277 | sourceTree = ""; 278 | }; 279 | 0DBA0BA122A4FAF6008685CD /* User Defaults Storage */ = { 280 | isa = PBXGroup; 281 | children = ( 282 | 0DC2F18B1993708A003A0131 /* MASDictionaryTransformer.h */, 283 | 0DC2F18C1993708A003A0131 /* MASDictionaryTransformer.m */, 284 | 0DC2F18F199372B4003A0131 /* MASDictionaryTransformerTests.m */, 285 | 0D827DAB199132840010B8EF /* MASShortcutBinder.h */, 286 | 0D827DAC199132840010B8EF /* MASShortcutBinder.m */, 287 | 0DC2F18819925F8F003A0131 /* MASShortcutBinderTests.m */, 288 | ); 289 | path = "User Defaults Storage"; 290 | sourceTree = ""; 291 | }; 292 | 0DBA0BA222A4FB15008685CD /* UI */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | 0D2CAB171B8339F4005431FC /* MASLocalization.h */, 296 | 0D2CAB181B8339F4005431FC /* MASLocalization.m */, 297 | 0D827D211990D55E0010B8EF /* MASShortcutView.h */, 298 | 0D827D221990D55E0010B8EF /* MASShortcutView.m */, 299 | 50C888EF26F8E2FE0086EB9A /* MASShortcutViewButtonCell.h */, 300 | 50C888F026F8E2FE0086EB9A /* MASShortcutViewButtonCell.m */, 301 | 0DC2F19619938EFA003A0131 /* MASShortcutView+Bindings.h */, 302 | 0DC2F19719938EFA003A0131 /* MASShortcutView+Bindings.m */, 303 | ); 304 | path = UI; 305 | sourceTree = ""; 306 | }; 307 | /* End PBXGroup section */ 308 | 309 | /* Begin PBXHeadersBuildPhase section */ 310 | 0D827CD01990D4420010B8EF /* Headers */ = { 311 | isa = PBXHeadersBuildPhase; 312 | buildActionMask = 2147483647; 313 | files = ( 314 | 0D827D9719910FF70010B8EF /* MASKeyCodes.h in Headers */, 315 | 0D827D2B1990D55E0010B8EF /* MASShortcutView.h in Headers */, 316 | 0D827D99199110F60010B8EF /* Prefix.pch in Headers */, 317 | 0D827D251990D55E0010B8EF /* MASShortcut.h in Headers */, 318 | 0DC2F19819938EFA003A0131 /* MASShortcutView+Bindings.h in Headers */, 319 | 0D827D9E19911A190010B8EF /* MASShortcutValidator.h in Headers */, 320 | 0DC2F18D1993708A003A0131 /* MASDictionaryTransformer.h in Headers */, 321 | 0D827DA519912D240010B8EF /* MASShortcutMonitor.h in Headers */, 322 | 0D827DAD199132840010B8EF /* MASShortcutBinder.h in Headers */, 323 | 0D2CAB191B8339F4005431FC /* MASLocalization.h in Headers */, 324 | 50C888F126F8E2FE0086EB9A /* MASShortcutViewButtonCell.h in Headers */, 325 | 0DC2F17619922798003A0131 /* MASHotKey.h in Headers */, 326 | 0D827D771990F81E0010B8EF /* Shortcut.h in Headers */, 327 | ); 328 | runOnlyForDeploymentPostprocessing = 0; 329 | }; 330 | /* End PBXHeadersBuildPhase section */ 331 | 332 | /* Begin PBXNativeTarget section */ 333 | 0D827CD21990D4420010B8EF /* MASShortcut */ = { 334 | isa = PBXNativeTarget; 335 | buildConfigurationList = 0D827CFB1990D4420010B8EF /* Build configuration list for PBXNativeTarget "MASShortcut" */; 336 | buildPhases = ( 337 | 0D827CCE1990D4420010B8EF /* Sources */, 338 | 0D827CCF1990D4420010B8EF /* Frameworks */, 339 | 0D827CD01990D4420010B8EF /* Headers */, 340 | 0D827CD11990D4420010B8EF /* Resources */, 341 | ); 342 | buildRules = ( 343 | ); 344 | dependencies = ( 345 | ); 346 | name = MASShortcut; 347 | productName = MASShortcut; 348 | productReference = 0D827CD31990D4420010B8EF /* MASShortcut.framework */; 349 | productType = "com.apple.product-type.framework"; 350 | }; 351 | 0D827D361990D5E70010B8EF /* Demo */ = { 352 | isa = PBXNativeTarget; 353 | buildConfigurationList = 0D827D661990D5E70010B8EF /* Build configuration list for PBXNativeTarget "Demo" */; 354 | buildPhases = ( 355 | 0D827D331990D5E70010B8EF /* Sources */, 356 | 0D827D341990D5E70010B8EF /* Frameworks */, 357 | 0D827D351990D5E70010B8EF /* Resources */, 358 | 0D827D741990D6980010B8EF /* Copy Frameworks */, 359 | ); 360 | buildRules = ( 361 | ); 362 | dependencies = ( 363 | ); 364 | name = Demo; 365 | productName = Demo; 366 | productReference = 0D827D371990D5E70010B8EF /* Demo.app */; 367 | productType = "com.apple.product-type.application"; 368 | }; 369 | 0D827D8219910AFF0010B8EF /* MASShortcutTests */ = { 370 | isa = PBXNativeTarget; 371 | buildConfigurationList = 0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "MASShortcutTests" */; 372 | buildPhases = ( 373 | 0D827D7F19910AFF0010B8EF /* Sources */, 374 | 0D827D8019910AFF0010B8EF /* Frameworks */, 375 | 0D827D8119910AFF0010B8EF /* Resources */, 376 | ); 377 | buildRules = ( 378 | ); 379 | dependencies = ( 380 | 0D827D8F19910AFF0010B8EF /* PBXTargetDependency */, 381 | ); 382 | name = MASShortcutTests; 383 | productName = Tests; 384 | productReference = 0D827D8319910AFF0010B8EF /* MASShortcutTests.xctest */; 385 | productType = "com.apple.product-type.bundle.unit-test"; 386 | }; 387 | /* End PBXNativeTarget section */ 388 | 389 | /* Begin PBXProject section */ 390 | 0D827CCA1990D4420010B8EF /* Project object */ = { 391 | isa = PBXProject; 392 | attributes = { 393 | LastUpgradeCheck = 1250; 394 | ORGANIZATIONNAME = "Vadim Shpakovski"; 395 | TargetAttributes = { 396 | 0D827D8219910AFF0010B8EF = { 397 | TestTargetID = 0D827CD21990D4420010B8EF; 398 | }; 399 | }; 400 | }; 401 | buildConfigurationList = 0D827CCD1990D4420010B8EF /* Build configuration list for PBXProject "MASShortcut" */; 402 | compatibilityVersion = "Xcode 8.0"; 403 | developmentRegion = en; 404 | hasScannedForEncodings = 0; 405 | knownRegions = ( 406 | en, 407 | cs, 408 | de, 409 | es, 410 | it, 411 | fr, 412 | ja, 413 | "zh-Hans", 414 | "zh-Hant", 415 | pl, 416 | ko, 417 | ru, 418 | nl, 419 | pt, 420 | sv, 421 | Base, 422 | ); 423 | mainGroup = 0D827CC91990D4420010B8EF; 424 | productRefGroup = 0D827CD41990D4420010B8EF /* Products */; 425 | projectDirPath = ""; 426 | projectRoot = ""; 427 | targets = ( 428 | 0D827CD21990D4420010B8EF /* MASShortcut */, 429 | 0D827D8219910AFF0010B8EF /* MASShortcutTests */, 430 | 0D827D361990D5E70010B8EF /* Demo */, 431 | ); 432 | }; 433 | /* End PBXProject section */ 434 | 435 | /* Begin PBXResourcesBuildPhase section */ 436 | 0D827CD11990D4420010B8EF /* Resources */ = { 437 | isa = PBXResourcesBuildPhase; 438 | buildActionMask = 2147483647; 439 | files = ( 440 | 0D2CAB131B8332E5005431FC /* Localizable.strings in Resources */, 441 | ); 442 | runOnlyForDeploymentPostprocessing = 0; 443 | }; 444 | 0D827D351990D5E70010B8EF /* Resources */ = { 445 | isa = PBXResourcesBuildPhase; 446 | buildActionMask = 2147483647; 447 | files = ( 448 | 0D2CAB1B1B83409C005431FC /* MainMenu.xib in Resources */, 449 | 0D2CAB211B834464005431FC /* Localizable.strings in Resources */, 450 | ); 451 | runOnlyForDeploymentPostprocessing = 0; 452 | }; 453 | 0D827D8119910AFF0010B8EF /* Resources */ = { 454 | isa = PBXResourcesBuildPhase; 455 | buildActionMask = 2147483647; 456 | files = ( 457 | ); 458 | runOnlyForDeploymentPostprocessing = 0; 459 | }; 460 | /* End PBXResourcesBuildPhase section */ 461 | 462 | /* Begin PBXSourcesBuildPhase section */ 463 | 0D827CCE1990D4420010B8EF /* Sources */ = { 464 | isa = PBXSourcesBuildPhase; 465 | buildActionMask = 2147483647; 466 | files = ( 467 | 0DC2F17719922798003A0131 /* MASHotKey.m in Sources */, 468 | 0D827D9F19911A190010B8EF /* MASShortcutValidator.m in Sources */, 469 | 0DC2F17C199232EA003A0131 /* MASShortcutMonitor.m in Sources */, 470 | 0D827D2C1990D55E0010B8EF /* MASShortcutView.m in Sources */, 471 | 0D827D261990D55E0010B8EF /* MASShortcut.m in Sources */, 472 | 0DC2F18E1993708A003A0131 /* MASDictionaryTransformer.m in Sources */, 473 | 0D2CAB1A1B8339F4005431FC /* MASLocalization.m in Sources */, 474 | 0DC2F19919938EFA003A0131 /* MASShortcutView+Bindings.m in Sources */, 475 | 50C888F226F8E2FE0086EB9A /* MASShortcutViewButtonCell.m in Sources */, 476 | 0DC2F17D199232F7003A0131 /* MASShortcutBinder.m in Sources */, 477 | ); 478 | runOnlyForDeploymentPostprocessing = 0; 479 | }; 480 | 0D827D331990D5E70010B8EF /* Sources */ = { 481 | isa = PBXSourcesBuildPhase; 482 | buildActionMask = 2147483647; 483 | files = ( 484 | 0D827D711990D6110010B8EF /* main.m in Sources */, 485 | 0D827D6F1990D6110010B8EF /* AppDelegate.m in Sources */, 486 | ); 487 | runOnlyForDeploymentPostprocessing = 0; 488 | }; 489 | 0D827D7F19910AFF0010B8EF /* Sources */ = { 490 | isa = PBXSourcesBuildPhase; 491 | buildActionMask = 2147483647; 492 | files = ( 493 | 0DC2F190199372B4003A0131 /* MASDictionaryTransformerTests.m in Sources */, 494 | 0D827D9419910B740010B8EF /* MASShortcutTests.m in Sources */, 495 | 0DC2F18919925F8F003A0131 /* MASShortcutBinderTests.m in Sources */, 496 | 0D39DCA21A668A4400639145 /* MASHotKeyTests.m in Sources */, 497 | 0D39DCA41A668E5500639145 /* MASShortcutMonitorTests.m in Sources */, 498 | ); 499 | runOnlyForDeploymentPostprocessing = 0; 500 | }; 501 | /* End PBXSourcesBuildPhase section */ 502 | 503 | /* Begin PBXTargetDependency section */ 504 | 0D827D8F19910AFF0010B8EF /* PBXTargetDependency */ = { 505 | isa = PBXTargetDependency; 506 | target = 0D827CD21990D4420010B8EF /* MASShortcut */; 507 | targetProxy = 0D827D8E19910AFF0010B8EF /* PBXContainerItemProxy */; 508 | }; 509 | /* End PBXTargetDependency section */ 510 | 511 | /* Begin PBXVariantGroup section */ 512 | 0D2CAB151B8332E5005431FC /* Localizable.strings */ = { 513 | isa = PBXVariantGroup; 514 | children = ( 515 | 0D2CAB141B8332E5005431FC /* en */, 516 | 0D2CAB161B8332EE005431FC /* cs */, 517 | 0D58DE521BA165FC0023BFBE /* de */, 518 | 0D58DE531BA166170023BFBE /* es */, 519 | 0D58DE541BA166270023BFBE /* it */, 520 | 0D58DE551BA166390023BFBE /* fr */, 521 | 0D58DE561BA166420023BFBE /* ja */, 522 | 76A0597D1C51DC940014B271 /* zh-Hans */, 523 | 76A0597E1C51DC9F0014B271 /* zh-Hant */, 524 | 6EA6034E1CBF822000A3ED9C /* pl */, 525 | 6EA6034F1CBF822800A3ED9C /* ko */, 526 | 6EA603501CBF822D00A3ED9C /* ru */, 527 | 6EA603511CBF823600A3ED9C /* nl */, 528 | 57B25C2D1E78E06D0061A9EC /* pt */, 529 | FDFF016F20CB2FB400CC88F3 /* sv */, 530 | ); 531 | name = Localizable.strings; 532 | sourceTree = ""; 533 | }; 534 | 0D2CAB1D1B83409C005431FC /* MainMenu.xib */ = { 535 | isa = PBXVariantGroup; 536 | children = ( 537 | 0D2CAB1E1B8340A4005431FC /* cs */, 538 | ED840EAE25E66B37003F76F7 /* Base */, 539 | ); 540 | name = MainMenu.xib; 541 | sourceTree = ""; 542 | }; 543 | 0D2CAB231B834464005431FC /* Localizable.strings */ = { 544 | isa = PBXVariantGroup; 545 | children = ( 546 | 0D2CAB221B834464005431FC /* en */, 547 | 0D2CAB241B834467005431FC /* cs */, 548 | ED8737791BCE459800BB1716 /* ja */, 549 | 0DEDAA021C6BB479001605F5 /* de */, 550 | 57B25C2E1E78E06D0061A9EC /* pt */, 551 | ); 552 | name = Localizable.strings; 553 | sourceTree = ""; 554 | }; 555 | /* End PBXVariantGroup section */ 556 | 557 | /* Begin XCBuildConfiguration section */ 558 | 0D827CF91990D4420010B8EF /* Debug */ = { 559 | isa = XCBuildConfiguration; 560 | buildSettings = { 561 | ALWAYS_SEARCH_USER_PATHS = NO; 562 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 563 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 564 | CLANG_CXX_LIBRARY = "libc++"; 565 | CLANG_ENABLE_MODULES = YES; 566 | CLANG_ENABLE_OBJC_ARC = YES; 567 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 568 | CLANG_WARN_BOOL_CONVERSION = YES; 569 | CLANG_WARN_COMMA = YES; 570 | CLANG_WARN_CONSTANT_CONVERSION = YES; 571 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 572 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 573 | CLANG_WARN_EMPTY_BODY = YES; 574 | CLANG_WARN_ENUM_CONVERSION = YES; 575 | CLANG_WARN_INFINITE_RECURSION = YES; 576 | CLANG_WARN_INT_CONVERSION = YES; 577 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 578 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 579 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 580 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 581 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; 582 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 583 | CLANG_WARN_STRICT_PROTOTYPES = YES; 584 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 585 | CLANG_WARN_UNREACHABLE_CODE = YES; 586 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 587 | COPY_PHASE_STRIP = NO; 588 | ENABLE_STRICT_OBJC_MSGSEND = YES; 589 | ENABLE_TESTABILITY = YES; 590 | GCC_C_LANGUAGE_STANDARD = gnu99; 591 | GCC_DYNAMIC_NO_PIC = NO; 592 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 593 | GCC_NO_COMMON_BLOCKS = YES; 594 | GCC_OPTIMIZATION_LEVEL = 0; 595 | GCC_PREPROCESSOR_DEFINITIONS = ( 596 | "DEBUG=1", 597 | "$(inherited)", 598 | ); 599 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 600 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 601 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 602 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 603 | GCC_WARN_UNDECLARED_SELECTOR = YES; 604 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 605 | GCC_WARN_UNUSED_FUNCTION = YES; 606 | GCC_WARN_UNUSED_VARIABLE = YES; 607 | MACOSX_DEPLOYMENT_TARGET = 10.10; 608 | ONLY_ACTIVE_ARCH = YES; 609 | SDKROOT = macosx; 610 | }; 611 | name = Debug; 612 | }; 613 | 0D827CFA1990D4420010B8EF /* Release */ = { 614 | isa = XCBuildConfiguration; 615 | buildSettings = { 616 | ALWAYS_SEARCH_USER_PATHS = NO; 617 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 618 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 619 | CLANG_CXX_LIBRARY = "libc++"; 620 | CLANG_ENABLE_MODULES = YES; 621 | CLANG_ENABLE_OBJC_ARC = YES; 622 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 623 | CLANG_WARN_BOOL_CONVERSION = YES; 624 | CLANG_WARN_COMMA = YES; 625 | CLANG_WARN_CONSTANT_CONVERSION = YES; 626 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 627 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 628 | CLANG_WARN_EMPTY_BODY = YES; 629 | CLANG_WARN_ENUM_CONVERSION = YES; 630 | CLANG_WARN_INFINITE_RECURSION = YES; 631 | CLANG_WARN_INT_CONVERSION = YES; 632 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 633 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 634 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 635 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 636 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; 637 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 638 | CLANG_WARN_STRICT_PROTOTYPES = YES; 639 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 640 | CLANG_WARN_UNREACHABLE_CODE = YES; 641 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 642 | COPY_PHASE_STRIP = YES; 643 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 644 | ENABLE_NS_ASSERTIONS = NO; 645 | ENABLE_STRICT_OBJC_MSGSEND = YES; 646 | GCC_C_LANGUAGE_STANDARD = gnu99; 647 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 648 | GCC_NO_COMMON_BLOCKS = YES; 649 | GCC_TREAT_WARNINGS_AS_ERRORS = YES; 650 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 651 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 652 | GCC_WARN_UNDECLARED_SELECTOR = YES; 653 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 654 | GCC_WARN_UNUSED_FUNCTION = YES; 655 | GCC_WARN_UNUSED_VARIABLE = YES; 656 | MACOSX_DEPLOYMENT_TARGET = 10.10; 657 | SDKROOT = macosx; 658 | }; 659 | name = Release; 660 | }; 661 | 0D827CFC1990D4420010B8EF /* Debug */ = { 662 | isa = XCBuildConfiguration; 663 | buildSettings = { 664 | APPLICATION_EXTENSION_API_ONLY = YES; 665 | COMBINE_HIDPI_IMAGES = YES; 666 | DEFINES_MODULE = YES; 667 | DYLIB_COMPATIBILITY_VERSION = 1; 668 | DYLIB_CURRENT_VERSION = 2.4.0; 669 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 670 | FRAMEWORK_VERSION = A; 671 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 672 | GCC_PREFIX_HEADER = Framework/Prefix.pch; 673 | INFOPLIST_FILE = Framework/Info.plist; 674 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 675 | MODULEMAP_FILE = Framework/MASShortcut.modulemap; 676 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}"; 677 | PRODUCT_NAME = "$(TARGET_NAME)"; 678 | SKIP_INSTALL = YES; 679 | USER_HEADER_SEARCH_PATHS = "\"$SRCROOT/Framework/include\""; 680 | USE_HEADERMAP = NO; 681 | WRAPPER_EXTENSION = framework; 682 | }; 683 | name = Debug; 684 | }; 685 | 0D827CFD1990D4420010B8EF /* Release */ = { 686 | isa = XCBuildConfiguration; 687 | buildSettings = { 688 | APPLICATION_EXTENSION_API_ONLY = YES; 689 | COMBINE_HIDPI_IMAGES = YES; 690 | DEFINES_MODULE = YES; 691 | DYLIB_COMPATIBILITY_VERSION = 1; 692 | DYLIB_CURRENT_VERSION = 2.4.0; 693 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 694 | FRAMEWORK_VERSION = A; 695 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 696 | GCC_PREFIX_HEADER = Framework/Prefix.pch; 697 | INFOPLIST_FILE = Framework/Info.plist; 698 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 699 | MODULEMAP_FILE = Framework/MASShortcut.modulemap; 700 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.shpakovski.${PRODUCT_NAME:rfc1034identifier}"; 701 | PRODUCT_NAME = "$(TARGET_NAME)"; 702 | SKIP_INSTALL = YES; 703 | USER_HEADER_SEARCH_PATHS = "\"$SRCROOT/Framework/include\""; 704 | USE_HEADERMAP = NO; 705 | WRAPPER_EXTENSION = framework; 706 | }; 707 | name = Release; 708 | }; 709 | 0D827D621990D5E70010B8EF /* Debug */ = { 710 | isa = XCBuildConfiguration; 711 | buildSettings = { 712 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 713 | CODE_SIGN_IDENTITY = "-"; 714 | COMBINE_HIDPI_IMAGES = YES; 715 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 716 | GCC_PREFIX_HEADER = Demo/Prefix.pch; 717 | GCC_PREPROCESSOR_DEFINITIONS = ( 718 | "DEBUG=1", 719 | "$(inherited)", 720 | ); 721 | INFOPLIST_FILE = Demo/Info.plist; 722 | MACOSX_DEPLOYMENT_TARGET = 10.11; 723 | PRODUCT_BUNDLE_IDENTIFIER = "com.shpakovski.mac.${PRODUCT_NAME:rfc1034identifier}"; 724 | PRODUCT_NAME = "$(TARGET_NAME)"; 725 | WRAPPER_EXTENSION = app; 726 | }; 727 | name = Debug; 728 | }; 729 | 0D827D631990D5E70010B8EF /* Release */ = { 730 | isa = XCBuildConfiguration; 731 | buildSettings = { 732 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 733 | CODE_SIGN_IDENTITY = "-"; 734 | COMBINE_HIDPI_IMAGES = YES; 735 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 736 | GCC_PREFIX_HEADER = Demo/Prefix.pch; 737 | INFOPLIST_FILE = Demo/Info.plist; 738 | MACOSX_DEPLOYMENT_TARGET = 10.11; 739 | PRODUCT_BUNDLE_IDENTIFIER = "com.shpakovski.mac.${PRODUCT_NAME:rfc1034identifier}"; 740 | PRODUCT_NAME = "$(TARGET_NAME)"; 741 | WRAPPER_EXTENSION = app; 742 | }; 743 | name = Release; 744 | }; 745 | 0D827D9019910AFF0010B8EF /* Debug */ = { 746 | isa = XCBuildConfiguration; 747 | buildSettings = { 748 | COMBINE_HIDPI_IMAGES = YES; 749 | FRAMEWORK_SEARCH_PATHS = ( 750 | "$(DEVELOPER_FRAMEWORKS_DIR)", 751 | "$(inherited)", 752 | ); 753 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 754 | GCC_PREFIX_HEADER = Tests/Prefix.pch; 755 | GCC_PREPROCESSOR_DEFINITIONS = ( 756 | "DEBUG=1", 757 | "$(inherited)", 758 | ); 759 | INFOPLIST_FILE = Tests/Info.plist; 760 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.shpakovski.MASShortcut.${PRODUCT_NAME:rfc1034identifier}"; 761 | PRODUCT_NAME = "$(TARGET_NAME)"; 762 | WRAPPER_EXTENSION = xctest; 763 | }; 764 | name = Debug; 765 | }; 766 | 0D827D9119910AFF0010B8EF /* Release */ = { 767 | isa = XCBuildConfiguration; 768 | buildSettings = { 769 | COMBINE_HIDPI_IMAGES = YES; 770 | FRAMEWORK_SEARCH_PATHS = ( 771 | "$(DEVELOPER_FRAMEWORKS_DIR)", 772 | "$(inherited)", 773 | ); 774 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 775 | GCC_PREFIX_HEADER = Tests/Prefix.pch; 776 | INFOPLIST_FILE = Tests/Info.plist; 777 | PRODUCT_BUNDLE_IDENTIFIER = "com.github.shpakovski.MASShortcut.${PRODUCT_NAME:rfc1034identifier}"; 778 | PRODUCT_NAME = "$(TARGET_NAME)"; 779 | WRAPPER_EXTENSION = xctest; 780 | }; 781 | name = Release; 782 | }; 783 | /* End XCBuildConfiguration section */ 784 | 785 | /* Begin XCConfigurationList section */ 786 | 0D827CCD1990D4420010B8EF /* Build configuration list for PBXProject "MASShortcut" */ = { 787 | isa = XCConfigurationList; 788 | buildConfigurations = ( 789 | 0D827CF91990D4420010B8EF /* Debug */, 790 | 0D827CFA1990D4420010B8EF /* Release */, 791 | ); 792 | defaultConfigurationIsVisible = 0; 793 | defaultConfigurationName = Release; 794 | }; 795 | 0D827CFB1990D4420010B8EF /* Build configuration list for PBXNativeTarget "MASShortcut" */ = { 796 | isa = XCConfigurationList; 797 | buildConfigurations = ( 798 | 0D827CFC1990D4420010B8EF /* Debug */, 799 | 0D827CFD1990D4420010B8EF /* Release */, 800 | ); 801 | defaultConfigurationIsVisible = 0; 802 | defaultConfigurationName = Release; 803 | }; 804 | 0D827D661990D5E70010B8EF /* Build configuration list for PBXNativeTarget "Demo" */ = { 805 | isa = XCConfigurationList; 806 | buildConfigurations = ( 807 | 0D827D621990D5E70010B8EF /* Debug */, 808 | 0D827D631990D5E70010B8EF /* Release */, 809 | ); 810 | defaultConfigurationIsVisible = 0; 811 | defaultConfigurationName = Release; 812 | }; 813 | 0D827D9219910AFF0010B8EF /* Build configuration list for PBXNativeTarget "MASShortcutTests" */ = { 814 | isa = XCConfigurationList; 815 | buildConfigurations = ( 816 | 0D827D9019910AFF0010B8EF /* Debug */, 817 | 0D827D9119910AFF0010B8EF /* Release */, 818 | ); 819 | defaultConfigurationIsVisible = 0; 820 | defaultConfigurationName = Release; 821 | }; 822 | /* End XCConfigurationList section */ 823 | }; 824 | rootObject = 0D827CCA1990D4420010B8EF /* Project object */; 825 | } 826 | --------------------------------------------------------------------------------