├── .github ├── CODEOWNERS └── workflows │ ├── Danger.yml │ ├── Swift-Build.yml │ └── Xcode-Build.yml ├── .gitignore ├── .gitmodules ├── .swiftlint.yml ├── Cartfile ├── Cartfile.resolved ├── Dangerfile ├── Documentation └── Upgrading_Magnet_2.md ├── Example ├── Example.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDETemplateMacros.plist └── Example │ ├── AppDelegate.swift │ ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json │ ├── Base.lproj │ └── MainMenu.xib │ ├── Example.entitlements │ └── Info.plist ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Lib ├── Magnet.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDETemplateMacros.plist │ │ └── xcschemes │ │ └── Magnet.xcscheme ├── Magnet │ ├── Extensions │ │ ├── CollectionExtension.swift │ │ ├── IntExtension.swift │ │ ├── KeyExtension.swift │ │ └── NSEventExtension.swift │ ├── HotKey.swift │ ├── HotKeyCenter.swift │ ├── Info.plist │ ├── KeyCombo.swift │ ├── Magnet.h │ └── ModifierEventHandler.swift └── MagnetTests │ ├── CollectionExtensionTests.swift │ ├── Fixtures │ ├── v2_0_0KeyCombo.swift │ └── v3_1_0KeyCombo.swift │ ├── Info.plist │ ├── KeyComboTests.swift │ └── ModifierEventHandlerTests.swift ├── Magnet.podspec ├── Magnet.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Package.swift └── README.md /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Econa77 2 | -------------------------------------------------------------------------------- /.github/workflows/Danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | danger: 11 | runs-on: macos-13 12 | # https://github.com/danger/danger/issues/1103 13 | if: (github.event.pull_request.head.repo.fork == false) 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | submodules: recursive 18 | fetch-depth: 0 19 | - name: Setup ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: '3.2' 23 | bundler-cache: true 24 | - name: Cache Mint 25 | uses: actions/cache@v3 26 | with: 27 | path: .mint 28 | key: ${{ runner.os }}-mint-${{ env.SWIFTLINT_VERSION }} 29 | restore-keys: | 30 | ${{ runner.os }}-mint- 31 | - name: Install Mint and Packages 32 | run: | 33 | brew install mint 34 | mint install realm/SwiftLint@${{ env.SWIFTLINT_VERSION }} 35 | - name: Run Danger 36 | run: bundle exec danger 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | env: 40 | MINT_PATH: .mint/lib 41 | MINT_LINK_PATH: .mint/bin 42 | SWIFTLINT_VERSION: 0.53.0 43 | DEVELOPER_DIR: /Applications/Xcode_15.0.app/Contents/Developer 44 | -------------------------------------------------------------------------------- /.github/workflows/Swift-Build.yml: -------------------------------------------------------------------------------- 1 | name: Swift-Build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [macos-12, macos-13] 9 | xcode: ['14.2', '15.0'] 10 | exclude: 11 | - os: macos-12 12 | xcode: '15.0' 13 | env: 14 | DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | - name: Build and Test 20 | run: swift test 21 | -------------------------------------------------------------------------------- /.github/workflows/Xcode-Build.yml: -------------------------------------------------------------------------------- 1 | name: Xcode-Build 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | matrix: 8 | os: [macos-12, macos-13] 9 | xcode: ['14.2', '15.0'] 10 | exclude: 11 | - os: macos-12 12 | xcode: '15.0' 13 | env: 14 | DEVELOPER_DIR: "/Applications/Xcode_${{ matrix.xcode }}.app/Contents/Developer" 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | - name: Build and Test 20 | run: | 21 | set -o pipefail 22 | xcodebuild build-for-testing test-without-building \ 23 | -workspace "$PROJECT" \ 24 | -scheme "$SCHEME" \ 25 | -sdk "$SDK" \ 26 | -configuration Debug \ 27 | ENABLE_TESTABILITY=YES | xcpretty -c; 28 | env: 29 | PROJECT: Magnet.xcworkspace 30 | SCHEME: Magnet 31 | SDK: macosx 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### https://raw.github.com/github/gitignore/c68eb91e0a72ca1efba128c33de9caea41281f19/Global/Xcode.gitignore 2 | 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## Build generated 8 | build/ 9 | DerivedData/ 10 | Magnet.xcodeproj 11 | 12 | ## Various settings 13 | *.pbxuser 14 | !default.pbxuser 15 | *.mode1v3 16 | !default.mode1v3 17 | *.mode2v3 18 | !default.mode2v3 19 | *.perspectivev3 20 | !default.perspectivev3 21 | xcuserdata/ 22 | 23 | ## Other 24 | *.moved-aside 25 | *.xccheckout 26 | *.xcscmblueprint 27 | 28 | ## Obj-C/Swift specific 29 | *.hmap 30 | *.ipa 31 | *.dSYM.zip 32 | *.dSYM 33 | 34 | ## Playgrounds 35 | timeline.xctimeline 36 | playground.xcworkspace 37 | 38 | # Swift Package Manager 39 | # 40 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 41 | # Packages/ 42 | # Package.pins 43 | # Package.resolved 44 | .build/ 45 | .swiftpm/ 46 | 47 | # CocoaPods 48 | # 49 | # We recommend against adding the Pods directory to your .gitignore. However 50 | # you should judge for yourself, the pros and cons are mentioned at: 51 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 52 | # 53 | # Pods/ 54 | # 55 | # Add this line if you want to avoid checking in source code from the Xcode workspace 56 | # *.xcworkspace 57 | 58 | # Carthage 59 | # 60 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 61 | # Carthage/Checkouts 62 | 63 | Carthage/Build 64 | 65 | # fastlane 66 | # 67 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 68 | # screenshots whenever they are needed. 69 | # For more information about the recommended setup visit: 70 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 71 | 72 | fastlane/report.xml 73 | fastlane/Preview.html 74 | fastlane/screenshots/**/*.png 75 | fastlane/test_output 76 | 77 | # Code Injection 78 | # 79 | # After new code Injection tools there's a generated folder /iOSInjectionProject 80 | # https://github.com/johnno1962/injectionforxcode 81 | 82 | iOSInjectionProject/ 83 | 84 | 85 | 86 | ### https://raw.github.com/github/gitignore/c68eb91e0a72ca1efba128c33de9caea41281f19/Ruby.gitignore 87 | 88 | *.gem 89 | *.rbc 90 | /.config 91 | /coverage/ 92 | /InstalledFiles 93 | /pkg/ 94 | /spec/reports/ 95 | /spec/examples.txt 96 | /test/tmp/ 97 | /test/version_tmp/ 98 | /tmp/ 99 | 100 | # Used by dotenv library to load environment variables. 101 | # .env 102 | 103 | ## Specific to RubyMotion: 104 | .dat* 105 | .repl_history 106 | build/ 107 | *.bridgesupport 108 | build-iPhoneOS/ 109 | build-iPhoneSimulator/ 110 | 111 | ## Specific to RubyMotion (use of CocoaPods): 112 | # 113 | # We recommend against adding the Pods directory to your .gitignore. However 114 | # you should judge for yourself, the pros and cons are mentioned at: 115 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 116 | # 117 | # vendor/Pods/ 118 | 119 | ## Documentation cache and generated files: 120 | /.yardoc/ 121 | /_yardoc/ 122 | /doc/ 123 | /rdoc/ 124 | 125 | ## Environment normalization: 126 | /.bundle/ 127 | /vendor/bundle 128 | /lib/bundler/man/ 129 | 130 | # for a library or gem, you might want to ignore these files since the code is 131 | # intended to run in multiple environments; otherwise, check them in: 132 | # Gemfile.lock 133 | # .ruby-version 134 | # .ruby-gemset 135 | 136 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 137 | .rvmrc 138 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/Sauce"] 2 | path = Carthage/Checkouts/Sauce 3 | url = https://github.com/Clipy/Sauce.git 4 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | included: 2 | - Lib/Magnet 3 | - Lib/MagnetTests 4 | opt_in_rules: 5 | - empty_count 6 | - explicit_init 7 | - closure_spacing 8 | - overridden_super_call 9 | - redundant_nil_coalescing 10 | - private_outlet 11 | - nimble_operator 12 | - operator_usage_whitespace 13 | - closure_end_indentation 14 | - first_where 15 | - prohibited_super_call 16 | - vertical_parameter_alignment_on_call 17 | - unneeded_parentheses_in_closure_argument 18 | - extension_access_modifier 19 | - literal_expression_end_indentation 20 | - joined_default_parameter 21 | - contains_over_first_not_nil 22 | - override_in_extension 23 | - private_action 24 | - quick_discouraged_call 25 | - quick_discouraged_focused_test 26 | - quick_discouraged_pending_test 27 | - single_test_class 28 | - sorted_first_last 29 | - yoda_condition 30 | disabled_rules: 31 | - nesting 32 | - function_parameter_count 33 | line_length: 300 34 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "Clipy/Sauce" 2 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Clipy/Sauce" "v2.4.0" 2 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | github.dismiss_out_of_range_messages 2 | swiftlint.config_file = '.swiftlint.yml' 3 | swiftlint.binary_path = '.mint/bin/swiftlint' 4 | swiftlint.lint_files(inline_mode: true) 5 | -------------------------------------------------------------------------------- /Documentation/Upgrading_Magnet_2.md: -------------------------------------------------------------------------------- 1 | ## Upgrading from Magnet v2.x to 3.x 2 | 3 | ### `KeyCombo` initializer 4 | The standard initializer, `KeyCombo(keyCode:carbonModifiers:)` & `KeyCombo(keyCode:cocoaModifiers:)`, is removed. 5 | This change was necessary to set the correct shortcut keys on non-QWERTY keyboards. 6 | 7 | If you want to keep the same settings, use the following initializer. 8 | 9 | ```swift 10 | KeyCombo(QWERTYKeyCode:carbonModifiers:) 11 | KeyCombo(QWERTYKeyCode:cocoaModifiers:) 12 | ``` 13 | 14 | The recommended initializer in the future is to use enum of [Key](https://github.com/Clipy/Sauce/blob/v2.0.2/Lib/Sauce/Key.swift) which is independent of keyCode. 15 | 16 | ```swift 17 | KeyCombo(key:carbonModifiers:) 18 | KeyCombo(key:cocoaModifiers:) 19 | ``` 20 | 21 | This will set the correct shortcut for any keyboard layout you use. 22 | 23 | ### About key code 24 | Previously `KeyCombo.keyCode` kept the KeyCode that was set during initialization. 25 | Starting from v3.0.0, `KeyCombo.currentKeyCode` returns the correct key code for the currently set keyboard layout. 26 | 27 | ```swift 28 | let keyCombo = KeyCombo(key: v, cocoaModifiers: [.shift]) 29 | ``` 30 | 31 | In QWERTY keyboard layout. 32 | 33 | ```swift 34 | print(keyCombo.currentKeyCode) // return 9 35 | ``` 36 | 37 | In Dvorak keyboard layout. 38 | 39 | ```swift 40 | print(keyCombo.currentKeyCode) // return 47 41 | ``` 42 | 43 | If you want to get the old `KeyCombo.keyCode`, change it to `KeyCombo.QWERTYKeyCode`. 44 | 45 | ### About characters 46 | Previously, `KeyCombo.characters` did not add modifiers correctly. 47 | As of v3.0.0, `KeyCombo.characters` returns a string that takes into account the following qualifier. 48 | 49 | ```swift 50 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.command]) 51 | print(keyCombo.characters) // return v 52 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.shift]) 53 | print(keyCombo.characters) // return V 54 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.option]) 55 | print(keyCombo.characters) // return √ 56 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.option, .shift]) 57 | print(keyCombo.characters) // return ◊ 58 | ``` 59 | 60 | If you want to get the same string as before, use `KeyCombo.keyEquivalent.uppercased()`. 61 | 62 | ```swift 63 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.command]) 64 | print(keyCombo.keyEquivalent) // return v 65 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.shift]) 66 | print(keyCombo.keyEquivalent) // return V 67 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.option]) 68 | print(keyCombo.keyEquivalent) // return v 69 | let keyCombo = KeyCombo(key: .v, cocoaModifiers: [.option, .shift]) 70 | print(keyCombo.keyEquivalent) // return V 71 | ``` 72 | 73 | **Important** 74 | `KeyCombo.keyEquivalent` is case-sensitive depending on whether the shift is typed or not. 75 | 76 | ### Removed `KeyCodeTransformer` and `KeyTransformer` 77 | - `KeyCodeTransformer.shared.transformValue(:)` This method has been migrated to the [Sauce.framework](https://github.com/Clipy/Sauce/tree/v2.0.0#character). 78 | - The methods of `KeyTransformer` have been migrated to their respective extensions. 79 | - https://github.com/Clipy/Magnet/blob/v3.0.4/Lib/Magnet/Extensions/IntExtension.swift 80 | - https://github.com/Clipy/Magnet/blob/v3.0.4/Lib/Magnet/Extensions/KeyExtension.swift 81 | - https://github.com/Clipy/Magnet/blob/v3.0.4/Lib/Magnet/Extensions/NSEventExtension.swift 82 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | FAEC34831C8F2299004177E2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34821C8F2299004177E2 /* AppDelegate.swift */; }; 11 | FAEC34851C8F2299004177E2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FAEC34841C8F2299004177E2 /* Assets.xcassets */; }; 12 | FAEC34881C8F2299004177E2 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = FAEC34861C8F2299004177E2 /* MainMenu.xib */; }; 13 | FAEC34D31C905A1E004177E2 /* Magnet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAEC34D01C905A17004177E2 /* Magnet.framework */; }; 14 | FAEC34D41C905A1E004177E2 /* Magnet.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FAEC34D01C905A17004177E2 /* Magnet.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXContainerItemProxy section */ 18 | FAEC34CF1C905A17004177E2 /* PBXContainerItemProxy */ = { 19 | isa = PBXContainerItemProxy; 20 | containerPortal = FAEC34CA1C905A17004177E2 /* Magnet.xcodeproj */; 21 | proxyType = 2; 22 | remoteGlobalIDString = FAEC34AF1C9059DF004177E2; 23 | remoteInfo = Magnet; 24 | }; 25 | FAEC34D11C905A17004177E2 /* PBXContainerItemProxy */ = { 26 | isa = PBXContainerItemProxy; 27 | containerPortal = FAEC34CA1C905A17004177E2 /* Magnet.xcodeproj */; 28 | proxyType = 2; 29 | remoteGlobalIDString = FAEC34B91C9059DF004177E2; 30 | remoteInfo = MagnetTests; 31 | }; 32 | FAEC34D51C905A1E004177E2 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = FAEC34CA1C905A17004177E2 /* Magnet.xcodeproj */; 35 | proxyType = 1; 36 | remoteGlobalIDString = FAEC34AE1C9059DF004177E2; 37 | remoteInfo = Magnet; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXCopyFilesBuildPhase section */ 42 | FAEC349C1C8F22C5004177E2 /* Embed Frameworks */ = { 43 | isa = PBXCopyFilesBuildPhase; 44 | buildActionMask = 2147483647; 45 | dstPath = ""; 46 | dstSubfolderSpec = 10; 47 | files = ( 48 | FAEC34D41C905A1E004177E2 /* Magnet.framework in Embed Frameworks */, 49 | ); 50 | name = "Embed Frameworks"; 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXCopyFilesBuildPhase section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 31B3E31F1D17CC4800635572 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Example.entitlements; sourceTree = ""; }; 57 | FAEC347F1C8F2299004177E2 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 58 | FAEC34821C8F2299004177E2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 59 | FAEC34841C8F2299004177E2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 60 | FAEC34871C8F2299004177E2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 61 | FAEC34891C8F2299004177E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | FAEC34CA1C905A17004177E2 /* Magnet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Magnet.xcodeproj; path = ../Lib/Magnet.xcodeproj; sourceTree = ""; }; 63 | /* End PBXFileReference section */ 64 | 65 | /* Begin PBXFrameworksBuildPhase section */ 66 | FAEC347C1C8F2299004177E2 /* Frameworks */ = { 67 | isa = PBXFrameworksBuildPhase; 68 | buildActionMask = 2147483647; 69 | files = ( 70 | FAEC34D31C905A1E004177E2 /* Magnet.framework in Frameworks */, 71 | ); 72 | runOnlyForDeploymentPostprocessing = 0; 73 | }; 74 | /* End PBXFrameworksBuildPhase section */ 75 | 76 | /* Begin PBXGroup section */ 77 | FAEC34761C8F2299004177E2 = { 78 | isa = PBXGroup; 79 | children = ( 80 | FAEC34811C8F2299004177E2 /* Example */, 81 | FAEC34801C8F2299004177E2 /* Products */, 82 | FAEC34CA1C905A17004177E2 /* Magnet.xcodeproj */, 83 | ); 84 | sourceTree = ""; 85 | }; 86 | FAEC34801C8F2299004177E2 /* Products */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | FAEC347F1C8F2299004177E2 /* Example.app */, 90 | ); 91 | name = Products; 92 | sourceTree = ""; 93 | }; 94 | FAEC34811C8F2299004177E2 /* Example */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 31B3E31F1D17CC4800635572 /* Example.entitlements */, 98 | FAEC34821C8F2299004177E2 /* AppDelegate.swift */, 99 | FAEC34841C8F2299004177E2 /* Assets.xcassets */, 100 | FAEC34861C8F2299004177E2 /* MainMenu.xib */, 101 | FAEC34891C8F2299004177E2 /* Info.plist */, 102 | ); 103 | path = Example; 104 | sourceTree = ""; 105 | }; 106 | FAEC34CB1C905A17004177E2 /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | FAEC34D01C905A17004177E2 /* Magnet.framework */, 110 | FAEC34D21C905A17004177E2 /* MagnetTests.xctest */, 111 | ); 112 | name = Products; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXNativeTarget section */ 118 | FAEC347E1C8F2299004177E2 /* Example */ = { 119 | isa = PBXNativeTarget; 120 | buildConfigurationList = FAEC348C1C8F2299004177E2 /* Build configuration list for PBXNativeTarget "Example" */; 121 | buildPhases = ( 122 | FAEC347B1C8F2299004177E2 /* Sources */, 123 | FAEC347C1C8F2299004177E2 /* Frameworks */, 124 | FAEC347D1C8F2299004177E2 /* Resources */, 125 | FAEC349C1C8F22C5004177E2 /* Embed Frameworks */, 126 | ); 127 | buildRules = ( 128 | ); 129 | dependencies = ( 130 | FAEC34D61C905A1E004177E2 /* PBXTargetDependency */, 131 | ); 132 | name = Example; 133 | productName = Example; 134 | productReference = FAEC347F1C8F2299004177E2 /* Example.app */; 135 | productType = "com.apple.product-type.application"; 136 | }; 137 | /* End PBXNativeTarget section */ 138 | 139 | /* Begin PBXProject section */ 140 | FAEC34771C8F2299004177E2 /* Project object */ = { 141 | isa = PBXProject; 142 | attributes = { 143 | LastSwiftUpdateCheck = 0730; 144 | LastUpgradeCheck = 1020; 145 | ORGANIZATIONNAME = "Clipy Project"; 146 | TargetAttributes = { 147 | FAEC347E1C8F2299004177E2 = { 148 | CreatedOnToolsVersion = 7.0.1; 149 | LastSwiftMigration = 0900; 150 | SystemCapabilities = { 151 | com.apple.Sandbox = { 152 | enabled = 1; 153 | }; 154 | }; 155 | }; 156 | }; 157 | }; 158 | buildConfigurationList = FAEC347A1C8F2299004177E2 /* Build configuration list for PBXProject "Example" */; 159 | compatibilityVersion = "Xcode 3.2"; 160 | developmentRegion = en; 161 | hasScannedForEncodings = 0; 162 | knownRegions = ( 163 | en, 164 | Base, 165 | ); 166 | mainGroup = FAEC34761C8F2299004177E2; 167 | productRefGroup = FAEC34801C8F2299004177E2 /* Products */; 168 | projectDirPath = ""; 169 | projectReferences = ( 170 | { 171 | ProductGroup = FAEC34CB1C905A17004177E2 /* Products */; 172 | ProjectRef = FAEC34CA1C905A17004177E2 /* Magnet.xcodeproj */; 173 | }, 174 | ); 175 | projectRoot = ""; 176 | targets = ( 177 | FAEC347E1C8F2299004177E2 /* Example */, 178 | ); 179 | }; 180 | /* End PBXProject section */ 181 | 182 | /* Begin PBXReferenceProxy section */ 183 | FAEC34D01C905A17004177E2 /* Magnet.framework */ = { 184 | isa = PBXReferenceProxy; 185 | fileType = wrapper.framework; 186 | path = Magnet.framework; 187 | remoteRef = FAEC34CF1C905A17004177E2 /* PBXContainerItemProxy */; 188 | sourceTree = BUILT_PRODUCTS_DIR; 189 | }; 190 | FAEC34D21C905A17004177E2 /* MagnetTests.xctest */ = { 191 | isa = PBXReferenceProxy; 192 | fileType = wrapper.cfbundle; 193 | path = MagnetTests.xctest; 194 | remoteRef = FAEC34D11C905A17004177E2 /* PBXContainerItemProxy */; 195 | sourceTree = BUILT_PRODUCTS_DIR; 196 | }; 197 | /* End PBXReferenceProxy section */ 198 | 199 | /* Begin PBXResourcesBuildPhase section */ 200 | FAEC347D1C8F2299004177E2 /* Resources */ = { 201 | isa = PBXResourcesBuildPhase; 202 | buildActionMask = 2147483647; 203 | files = ( 204 | FAEC34851C8F2299004177E2 /* Assets.xcassets in Resources */, 205 | FAEC34881C8F2299004177E2 /* MainMenu.xib in Resources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXResourcesBuildPhase section */ 210 | 211 | /* Begin PBXSourcesBuildPhase section */ 212 | FAEC347B1C8F2299004177E2 /* Sources */ = { 213 | isa = PBXSourcesBuildPhase; 214 | buildActionMask = 2147483647; 215 | files = ( 216 | FAEC34831C8F2299004177E2 /* AppDelegate.swift in Sources */, 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | }; 220 | /* End PBXSourcesBuildPhase section */ 221 | 222 | /* Begin PBXTargetDependency section */ 223 | FAEC34D61C905A1E004177E2 /* PBXTargetDependency */ = { 224 | isa = PBXTargetDependency; 225 | name = Magnet; 226 | targetProxy = FAEC34D51C905A1E004177E2 /* PBXContainerItemProxy */; 227 | }; 228 | /* End PBXTargetDependency section */ 229 | 230 | /* Begin PBXVariantGroup section */ 231 | FAEC34861C8F2299004177E2 /* MainMenu.xib */ = { 232 | isa = PBXVariantGroup; 233 | children = ( 234 | FAEC34871C8F2299004177E2 /* Base */, 235 | ); 236 | name = MainMenu.xib; 237 | sourceTree = ""; 238 | }; 239 | /* End PBXVariantGroup section */ 240 | 241 | /* Begin XCBuildConfiguration section */ 242 | FAEC348A1C8F2299004177E2 /* Debug */ = { 243 | isa = XCBuildConfiguration; 244 | buildSettings = { 245 | ALWAYS_SEARCH_USER_PATHS = NO; 246 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 247 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 248 | CLANG_CXX_LIBRARY = "libc++"; 249 | CLANG_ENABLE_MODULES = YES; 250 | CLANG_ENABLE_OBJC_ARC = YES; 251 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 252 | CLANG_WARN_BOOL_CONVERSION = YES; 253 | CLANG_WARN_COMMA = YES; 254 | CLANG_WARN_CONSTANT_CONVERSION = YES; 255 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 256 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INFINITE_RECURSION = YES; 260 | CLANG_WARN_INT_CONVERSION = YES; 261 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 262 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 263 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 264 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNREACHABLE_CODE = YES; 269 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 270 | CODE_SIGN_IDENTITY = "-"; 271 | COPY_PHASE_STRIP = NO; 272 | DEBUG_INFORMATION_FORMAT = dwarf; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_DYNAMIC_NO_PIC = NO; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_OPTIMIZATION_LEVEL = 0; 279 | GCC_PREPROCESSOR_DEFINITIONS = ( 280 | "DEBUG=1", 281 | "$(inherited)", 282 | ); 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | MACOSX_DEPLOYMENT_TARGET = 10.13; 290 | MTL_ENABLE_DEBUG_INFO = YES; 291 | ONLY_ACTIVE_ARCH = YES; 292 | SDKROOT = macosx; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | SWIFT_VERSION = 3.0; 295 | }; 296 | name = Debug; 297 | }; 298 | FAEC348B1C8F2299004177E2 /* Release */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 303 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 304 | CLANG_CXX_LIBRARY = "libc++"; 305 | CLANG_ENABLE_MODULES = YES; 306 | CLANG_ENABLE_OBJC_ARC = YES; 307 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 308 | CLANG_WARN_BOOL_CONVERSION = YES; 309 | CLANG_WARN_COMMA = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 312 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 313 | CLANG_WARN_EMPTY_BODY = YES; 314 | CLANG_WARN_ENUM_CONVERSION = YES; 315 | CLANG_WARN_INFINITE_RECURSION = YES; 316 | CLANG_WARN_INT_CONVERSION = YES; 317 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 318 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 319 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 320 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 321 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 322 | CLANG_WARN_STRICT_PROTOTYPES = YES; 323 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 324 | CLANG_WARN_UNREACHABLE_CODE = YES; 325 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 326 | CODE_SIGN_IDENTITY = "-"; 327 | COPY_PHASE_STRIP = NO; 328 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 329 | ENABLE_NS_ASSERTIONS = NO; 330 | ENABLE_STRICT_OBJC_MSGSEND = YES; 331 | GCC_C_LANGUAGE_STANDARD = gnu99; 332 | GCC_NO_COMMON_BLOCKS = YES; 333 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 334 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 335 | GCC_WARN_UNDECLARED_SELECTOR = YES; 336 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 337 | GCC_WARN_UNUSED_FUNCTION = YES; 338 | GCC_WARN_UNUSED_VARIABLE = YES; 339 | MACOSX_DEPLOYMENT_TARGET = 10.13; 340 | MTL_ENABLE_DEBUG_INFO = NO; 341 | SDKROOT = macosx; 342 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 343 | SWIFT_VERSION = 3.0; 344 | }; 345 | name = Release; 346 | }; 347 | FAEC348D1C8F2299004177E2 /* Debug */ = { 348 | isa = XCBuildConfiguration; 349 | buildSettings = { 350 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 351 | CLANG_ENABLE_MODULES = YES; 352 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 353 | COMBINE_HIDPI_IMAGES = YES; 354 | INFOPLIST_FILE = Example/Info.plist; 355 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 356 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Example"; 357 | PRODUCT_NAME = "$(TARGET_NAME)"; 358 | SWIFT_OBJC_BRIDGING_HEADER = ""; 359 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 360 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 361 | SWIFT_VERSION = 4.0; 362 | }; 363 | name = Debug; 364 | }; 365 | FAEC348E1C8F2299004177E2 /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | CLANG_ENABLE_MODULES = YES; 370 | CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; 371 | COMBINE_HIDPI_IMAGES = YES; 372 | INFOPLIST_FILE = Example/Info.plist; 373 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 374 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Example"; 375 | PRODUCT_NAME = "$(TARGET_NAME)"; 376 | SWIFT_OBJC_BRIDGING_HEADER = ""; 377 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 378 | SWIFT_VERSION = 4.0; 379 | }; 380 | name = Release; 381 | }; 382 | /* End XCBuildConfiguration section */ 383 | 384 | /* Begin XCConfigurationList section */ 385 | FAEC347A1C8F2299004177E2 /* Build configuration list for PBXProject "Example" */ = { 386 | isa = XCConfigurationList; 387 | buildConfigurations = ( 388 | FAEC348A1C8F2299004177E2 /* Debug */, 389 | FAEC348B1C8F2299004177E2 /* Release */, 390 | ); 391 | defaultConfigurationIsVisible = 0; 392 | defaultConfigurationName = Release; 393 | }; 394 | FAEC348C1C8F2299004177E2 /* Build configuration list for PBXNativeTarget "Example" */ = { 395 | isa = XCConfigurationList; 396 | buildConfigurations = ( 397 | FAEC348D1C8F2299004177E2 /* Debug */, 398 | FAEC348E1C8F2299004177E2 /* Release */, 399 | ); 400 | defaultConfigurationIsVisible = 0; 401 | defaultConfigurationName = Release; 402 | }; 403 | /* End XCConfigurationList section */ 404 | }; 405 | rootObject = FAEC34771C8F2299004177E2 /* Project object */; 406 | } 407 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // 9 | // ___PACKAGENAME___ 10 | // GitHub: ___REPOSITORYURL___ 11 | // HP: ___PRODUCTURL___ 12 | // 13 | // Copyright © 2015-___YEAR___ Clipy Project. 14 | // 15 | REPOSITORYURL 16 | https://github.com/clipy 17 | PRODUCTURL 18 | https://clipy-app.com 19 | 20 | 21 | -------------------------------------------------------------------------------- /Example/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // 4 | // Example 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | import Magnet 13 | import Carbon 14 | 15 | @NSApplicationMain 16 | class AppDelegate: NSObject, NSApplicationDelegate { 17 | 18 | @IBOutlet weak var window: NSWindow! 19 | 20 | func applicationDidFinishLaunching(_ aNotification: Notification) { 21 | // ⌘ + Control + B 22 | guard let keyCombo = KeyCombo(key: .b, cocoaModifiers: [.command, .control]) else { return } 23 | let hotKey = HotKey(identifier: "CommandControlB", 24 | keyCombo: keyCombo, 25 | target: self, 26 | action: #selector(AppDelegate.tappedHotKey)) 27 | hotKey.register() 28 | 29 | // Shift + Control + A 30 | guard let keyCombo2 = KeyCombo(key: .a, cocoaModifiers: [.shift, .control]) else { return } 31 | let hotKey2 = HotKey(identifier: "ShiftControlA", 32 | keyCombo: keyCombo2, 33 | target: self, 34 | action: #selector(AppDelegate.tappedHotKey2)) 35 | hotKey2.register() 36 | 37 | // ⌘ Double Tap 38 | guard let keyCombo3 = KeyCombo(doubledCocoaModifiers: .command) else { return } 39 | let hotKey3 = HotKey(identifier: "CommandDoubleTap", 40 | keyCombo: keyCombo3, 41 | target: self, 42 | action: #selector(AppDelegate.tappedDoubleCommandKey)) 43 | hotKey3.register() 44 | 45 | // Shift Double Tap 46 | guard let keyCombo4 = KeyCombo(doubledCarbonModifiers: shiftKey) else { return } 47 | let hotKey4 = HotKey(identifier: "ShiftDoubleTap", 48 | keyCombo: keyCombo4, 49 | target: self, 50 | action: #selector(AppDelegate.tappedDoubleShiftKey)) 51 | hotKey4.register() 52 | 53 | // Control Double Tap 54 | guard let keyCombo5 = KeyCombo(doubledCocoaModifiers: .control) else { return } 55 | let hotKey5 = HotKey(identifier: "ControlDoubleTap", keyCombo: keyCombo5) { _ in 56 | print("control double tapped!!!") 57 | } 58 | hotKey5.register() 59 | 60 | // Option Double Tap 61 | guard let keyCombo6 = KeyCombo(doubledCocoaModifiers: .option) else { return } 62 | let hotKey6 = HotKey(identifier: "OptionDoubleTap", keyCombo: keyCombo6) { _ in 63 | print("option double tapped!!!") 64 | } 65 | hotKey6.register() 66 | } 67 | 68 | @objc func tappedHotKey() { 69 | print("hotKey!!!!") 70 | } 71 | 72 | @objc func tappedHotKey2() { 73 | print("hotKey2!!!!") 74 | } 75 | 76 | @objc func tappedDoubleCommandKey() { 77 | print("command double tapped!!!") 78 | } 79 | 80 | @objc func tappedDoubleShiftKey() { 81 | print("shift double tapped!!!") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Example/Example/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | Default 540 | 541 | 542 | 543 | 544 | 545 | 546 | Left to Right 547 | 548 | 549 | 550 | 551 | 552 | 553 | Right to Left 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | Default 565 | 566 | 567 | 568 | 569 | 570 | 571 | Left to Right 572 | 573 | 574 | 575 | 576 | 577 | 578 | Right to Left 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | -------------------------------------------------------------------------------- /Example/Example/Example.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSHumanReadableCopyright 28 | Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'cocoapods' 4 | gem "danger" 5 | gem "danger-swiftlint" 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.6) 5 | rexml 6 | activesupport (7.1.0) 7 | base64 8 | bigdecimal 9 | concurrent-ruby (~> 1.0, >= 1.0.2) 10 | connection_pool (>= 2.2.5) 11 | drb 12 | i18n (>= 1.6, < 2) 13 | minitest (>= 5.1) 14 | mutex_m 15 | tzinfo (~> 2.0) 16 | addressable (2.8.5) 17 | public_suffix (>= 2.0.2, < 6.0) 18 | algoliasearch (1.27.5) 19 | httpclient (~> 2.8, >= 2.8.3) 20 | json (>= 1.5.1) 21 | atomos (0.1.3) 22 | base64 (0.1.1) 23 | bigdecimal (3.1.4) 24 | claide (1.1.0) 25 | claide-plugins (0.9.2) 26 | cork 27 | nap 28 | open4 (~> 1.3) 29 | cocoapods (1.13.0) 30 | addressable (~> 2.8) 31 | claide (>= 1.0.2, < 2.0) 32 | cocoapods-core (= 1.13.0) 33 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 34 | cocoapods-downloader (>= 1.6.0, < 2.0) 35 | cocoapods-plugins (>= 1.0.0, < 2.0) 36 | cocoapods-search (>= 1.0.0, < 2.0) 37 | cocoapods-trunk (>= 1.6.0, < 2.0) 38 | cocoapods-try (>= 1.1.0, < 2.0) 39 | colored2 (~> 3.1) 40 | escape (~> 0.0.4) 41 | fourflusher (>= 2.3.0, < 3.0) 42 | gh_inspector (~> 1.0) 43 | molinillo (~> 0.8.0) 44 | nap (~> 1.0) 45 | ruby-macho (>= 2.3.0, < 3.0) 46 | xcodeproj (>= 1.23.0, < 2.0) 47 | cocoapods-core (1.13.0) 48 | activesupport (>= 5.0, < 8) 49 | addressable (~> 2.8) 50 | algoliasearch (~> 1.0) 51 | concurrent-ruby (~> 1.1) 52 | fuzzy_match (~> 2.0.4) 53 | nap (~> 1.0) 54 | netrc (~> 0.11) 55 | public_suffix (~> 4.0) 56 | typhoeus (~> 1.0) 57 | cocoapods-deintegrate (1.0.5) 58 | cocoapods-downloader (1.6.3) 59 | cocoapods-plugins (1.0.0) 60 | nap 61 | cocoapods-search (1.0.1) 62 | cocoapods-trunk (1.6.0) 63 | nap (>= 0.8, < 2.0) 64 | netrc (~> 0.11) 65 | cocoapods-try (1.2.0) 66 | colored2 (3.1.2) 67 | concurrent-ruby (1.2.2) 68 | connection_pool (2.4.1) 69 | cork (0.3.0) 70 | colored2 (~> 3.1) 71 | danger (9.3.2) 72 | claide (~> 1.0) 73 | claide-plugins (>= 0.9.2) 74 | colored2 (~> 3.1) 75 | cork (~> 0.1) 76 | faraday (>= 0.9.0, < 3.0) 77 | faraday-http-cache (~> 2.0) 78 | git (~> 1.13) 79 | kramdown (~> 2.3) 80 | kramdown-parser-gfm (~> 1.0) 81 | no_proxy_fix 82 | octokit (~> 6.0) 83 | terminal-table (>= 1, < 4) 84 | danger-swiftlint (0.33.0) 85 | danger 86 | rake (> 10) 87 | thor (~> 0.19) 88 | drb (2.1.1) 89 | ruby2_keywords 90 | escape (0.0.4) 91 | ethon (0.16.0) 92 | ffi (>= 1.15.0) 93 | faraday (2.7.11) 94 | base64 95 | faraday-net_http (>= 2.0, < 3.1) 96 | ruby2_keywords (>= 0.0.4) 97 | faraday-http-cache (2.5.0) 98 | faraday (>= 0.8) 99 | faraday-net_http (3.0.2) 100 | ffi (1.16.3) 101 | fourflusher (2.3.1) 102 | fuzzy_match (2.0.4) 103 | gh_inspector (1.1.3) 104 | git (1.18.0) 105 | addressable (~> 2.8) 106 | rchardet (~> 1.8) 107 | httpclient (2.8.3) 108 | i18n (1.14.1) 109 | concurrent-ruby (~> 1.0) 110 | json (2.6.3) 111 | kramdown (2.4.0) 112 | rexml 113 | kramdown-parser-gfm (1.1.0) 114 | kramdown (~> 2.0) 115 | minitest (5.20.0) 116 | molinillo (0.8.0) 117 | mutex_m (0.1.2) 118 | nanaimo (0.3.0) 119 | nap (1.1.0) 120 | netrc (0.11.0) 121 | no_proxy_fix (0.1.2) 122 | octokit (6.1.1) 123 | faraday (>= 1, < 3) 124 | sawyer (~> 0.9) 125 | open4 (1.3.4) 126 | public_suffix (4.0.7) 127 | rake (13.0.6) 128 | rchardet (1.8.0) 129 | rexml (3.3.9) 130 | ruby-macho (2.5.1) 131 | ruby2_keywords (0.0.5) 132 | sawyer (0.9.2) 133 | addressable (>= 2.3.5) 134 | faraday (>= 0.17.3, < 3) 135 | terminal-table (3.0.2) 136 | unicode-display_width (>= 1.1.1, < 3) 137 | thor (0.20.3) 138 | typhoeus (1.4.0) 139 | ethon (>= 0.9.0) 140 | tzinfo (2.0.6) 141 | concurrent-ruby (~> 1.0) 142 | unicode-display_width (2.5.0) 143 | xcodeproj (1.25.1) 144 | CFPropertyList (>= 2.3.3, < 4.0) 145 | atomos (~> 0.1.3) 146 | claide (>= 1.0.2, < 2.0) 147 | colored2 (~> 3.1) 148 | nanaimo (~> 0.3.0) 149 | rexml (>= 3.3.6, < 4.0) 150 | 151 | PLATFORMS 152 | ruby 153 | 154 | DEPENDENCIES 155 | cocoapods 156 | danger 157 | danger-swiftlint 158 | 159 | BUNDLED WITH 160 | 2.3.5 161 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2020 Clipy Project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Lib/Magnet.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 090077D4245D449F0099B20A /* KeyComboTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 090077D3245D449F0099B20A /* KeyComboTests.swift */; }; 11 | 091D85EA245A917500930473 /* NSEventExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E6245A917500930473 /* NSEventExtension.swift */; }; 12 | 091D85EB245A917500930473 /* IntExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E7245A917500930473 /* IntExtension.swift */; }; 13 | 091D85EC245A917500930473 /* KeyExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E8245A917500930473 /* KeyExtension.swift */; }; 14 | 091D85ED245A917500930473 /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 091D85E9245A917500930473 /* CollectionExtension.swift */; }; 15 | 099F29FA246CFA9500992925 /* v2_0_0KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F29F9246CFA9500992925 /* v2_0_0KeyCombo.swift */; }; 16 | 099F2A02246D091400992925 /* ModifierEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F29FF246D091400992925 /* ModifierEventHandler.swift */; }; 17 | 099F2A07246D094500992925 /* ModifierEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099F2A04246D094500992925 /* ModifierEventHandlerTests.swift */; }; 18 | 09DEE124245B128C00169BEC /* Sauce.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 091D85CF2459553A00930473 /* Sauce.framework */; }; 19 | FA3AA2162315A6A3007EAA1F /* CollectionExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */; }; 20 | FADEA45A24796103003AEC83 /* v3_1_0KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADEA45924796103003AEC83 /* v3_1_0KeyCombo.swift */; }; 21 | FAEC34B31C9059DF004177E2 /* Magnet.h in Headers */ = {isa = PBXBuildFile; fileRef = FAEC34B21C9059DF004177E2 /* Magnet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 22 | FAEC34BA1C9059DF004177E2 /* Magnet.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FAEC34AF1C9059DF004177E2 /* Magnet.framework */; }; 23 | FAEC34D81C905B4D004177E2 /* HotKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34D71C905B4D004177E2 /* HotKey.swift */; }; 24 | FAEC34DA1C905B5A004177E2 /* KeyCombo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34D91C905B5A004177E2 /* KeyCombo.swift */; }; 25 | FAEC34DE1C905B7C004177E2 /* HotKeyCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 091D85CE2459553A00930473 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 091D85C92459553A00930473 /* Sauce.xcodeproj */; 32 | proxyType = 2; 33 | remoteGlobalIDString = FAA41320210E2C730097D522; 34 | remoteInfo = Sauce; 35 | }; 36 | 091D85D02459553A00930473 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 091D85C92459553A00930473 /* Sauce.xcodeproj */; 39 | proxyType = 2; 40 | remoteGlobalIDString = FAA41329210E2C730097D522; 41 | remoteInfo = SauceTests; 42 | }; 43 | FAEC34BB1C9059DF004177E2 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = FAEC34A61C9059DF004177E2 /* Project object */; 46 | proxyType = 1; 47 | remoteGlobalIDString = FAEC34AE1C9059DF004177E2; 48 | remoteInfo = Magnet; 49 | }; 50 | /* End PBXContainerItemProxy section */ 51 | 52 | /* Begin PBXFileReference section */ 53 | 090077D3245D449F0099B20A /* KeyComboTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyComboTests.swift; sourceTree = ""; }; 54 | 091D85C92459553A00930473 /* Sauce.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Sauce.xcodeproj; path = ../Carthage/Checkouts/Sauce/Lib/Sauce.xcodeproj; sourceTree = ""; }; 55 | 091D85E6245A917500930473 /* NSEventExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSEventExtension.swift; sourceTree = ""; }; 56 | 091D85E7245A917500930473 /* IntExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntExtension.swift; sourceTree = ""; }; 57 | 091D85E8245A917500930473 /* KeyExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyExtension.swift; sourceTree = ""; }; 58 | 091D85E9245A917500930473 /* CollectionExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; 59 | 099F29F9246CFA9500992925 /* v2_0_0KeyCombo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = v2_0_0KeyCombo.swift; sourceTree = ""; }; 60 | 099F29FF246D091400992925 /* ModifierEventHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifierEventHandler.swift; sourceTree = ""; }; 61 | 099F2A04246D094500992925 /* ModifierEventHandlerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModifierEventHandlerTests.swift; sourceTree = ""; }; 62 | FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensionTests.swift; sourceTree = ""; }; 63 | FADEA45924796103003AEC83 /* v3_1_0KeyCombo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = v3_1_0KeyCombo.swift; sourceTree = ""; }; 64 | FAEC34AF1C9059DF004177E2 /* Magnet.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Magnet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | FAEC34B21C9059DF004177E2 /* Magnet.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Magnet.h; sourceTree = ""; }; 66 | FAEC34B41C9059DF004177E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | FAEC34B91C9059DF004177E2 /* MagnetTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MagnetTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | FAEC34C01C9059DF004177E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 69 | FAEC34D71C905B4D004177E2 /* HotKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKey.swift; sourceTree = ""; }; 70 | FAEC34D91C905B5A004177E2 /* KeyCombo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyCombo.swift; sourceTree = ""; }; 71 | FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HotKeyCenter.swift; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | FAEC34AB1C9059DF004177E2 /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | 09DEE124245B128C00169BEC /* Sauce.framework in Frameworks */, 80 | ); 81 | runOnlyForDeploymentPostprocessing = 0; 82 | }; 83 | FAEC34B61C9059DF004177E2 /* Frameworks */ = { 84 | isa = PBXFrameworksBuildPhase; 85 | buildActionMask = 2147483647; 86 | files = ( 87 | FAEC34BA1C9059DF004177E2 /* Magnet.framework in Frameworks */, 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | /* End PBXFrameworksBuildPhase section */ 92 | 93 | /* Begin PBXGroup section */ 94 | 091D85C82459552300930473 /* Frameworks */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | 091D85C92459553A00930473 /* Sauce.xcodeproj */, 98 | ); 99 | name = Frameworks; 100 | sourceTree = ""; 101 | }; 102 | 091D85CA2459553A00930473 /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 091D85CF2459553A00930473 /* Sauce.framework */, 106 | 091D85D12459553A00930473 /* SauceTests.xctest */, 107 | ); 108 | name = Products; 109 | sourceTree = ""; 110 | }; 111 | 091D85E5245A915A00930473 /* Extensions */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 091D85E9245A917500930473 /* CollectionExtension.swift */, 115 | 091D85E7245A917500930473 /* IntExtension.swift */, 116 | 091D85E8245A917500930473 /* KeyExtension.swift */, 117 | 091D85E6245A917500930473 /* NSEventExtension.swift */, 118 | ); 119 | path = Extensions; 120 | sourceTree = ""; 121 | }; 122 | 099F29F8246CFA8700992925 /* Fixtures */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | 099F29F9246CFA9500992925 /* v2_0_0KeyCombo.swift */, 126 | FADEA45924796103003AEC83 /* v3_1_0KeyCombo.swift */, 127 | ); 128 | path = Fixtures; 129 | sourceTree = ""; 130 | }; 131 | FAEC34A51C9059DF004177E2 = { 132 | isa = PBXGroup; 133 | children = ( 134 | FAEC34B11C9059DF004177E2 /* Magnet */, 135 | FAEC34BD1C9059DF004177E2 /* MagnetTests */, 136 | FAEC34B01C9059DF004177E2 /* Products */, 137 | 091D85C82459552300930473 /* Frameworks */, 138 | ); 139 | sourceTree = ""; 140 | }; 141 | FAEC34B01C9059DF004177E2 /* Products */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | FAEC34AF1C9059DF004177E2 /* Magnet.framework */, 145 | FAEC34B91C9059DF004177E2 /* MagnetTests.xctest */, 146 | ); 147 | name = Products; 148 | sourceTree = ""; 149 | }; 150 | FAEC34B11C9059DF004177E2 /* Magnet */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 091D85E5245A915A00930473 /* Extensions */, 154 | FAEC34D71C905B4D004177E2 /* HotKey.swift */, 155 | FAEC34D91C905B5A004177E2 /* KeyCombo.swift */, 156 | FAEC34DD1C905B7C004177E2 /* HotKeyCenter.swift */, 157 | 099F29FF246D091400992925 /* ModifierEventHandler.swift */, 158 | FAEC34B21C9059DF004177E2 /* Magnet.h */, 159 | FAEC34B41C9059DF004177E2 /* Info.plist */, 160 | ); 161 | path = Magnet; 162 | sourceTree = ""; 163 | }; 164 | FAEC34BD1C9059DF004177E2 /* MagnetTests */ = { 165 | isa = PBXGroup; 166 | children = ( 167 | 099F29F8246CFA8700992925 /* Fixtures */, 168 | FA3AA2152315A6A3007EAA1F /* CollectionExtensionTests.swift */, 169 | 090077D3245D449F0099B20A /* KeyComboTests.swift */, 170 | 099F2A04246D094500992925 /* ModifierEventHandlerTests.swift */, 171 | FAEC34C01C9059DF004177E2 /* Info.plist */, 172 | ); 173 | path = MagnetTests; 174 | sourceTree = SOURCE_ROOT; 175 | }; 176 | /* End PBXGroup section */ 177 | 178 | /* Begin PBXHeadersBuildPhase section */ 179 | FAEC34AC1C9059DF004177E2 /* Headers */ = { 180 | isa = PBXHeadersBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | FAEC34B31C9059DF004177E2 /* Magnet.h in Headers */, 184 | ); 185 | runOnlyForDeploymentPostprocessing = 0; 186 | }; 187 | /* End PBXHeadersBuildPhase section */ 188 | 189 | /* Begin PBXNativeTarget section */ 190 | FAEC34AE1C9059DF004177E2 /* Magnet */ = { 191 | isa = PBXNativeTarget; 192 | buildConfigurationList = FAEC34C31C9059DF004177E2 /* Build configuration list for PBXNativeTarget "Magnet" */; 193 | buildPhases = ( 194 | FAEC34AA1C9059DF004177E2 /* Sources */, 195 | FAEC34AB1C9059DF004177E2 /* Frameworks */, 196 | FAEC34AC1C9059DF004177E2 /* Headers */, 197 | FAEC34AD1C9059DF004177E2 /* Resources */, 198 | ); 199 | buildRules = ( 200 | ); 201 | dependencies = ( 202 | ); 203 | name = Magnet; 204 | productName = Magnet; 205 | productReference = FAEC34AF1C9059DF004177E2 /* Magnet.framework */; 206 | productType = "com.apple.product-type.framework"; 207 | }; 208 | FAEC34B81C9059DF004177E2 /* MagnetTests */ = { 209 | isa = PBXNativeTarget; 210 | buildConfigurationList = FAEC34C61C9059DF004177E2 /* Build configuration list for PBXNativeTarget "MagnetTests" */; 211 | buildPhases = ( 212 | FAEC34B51C9059DF004177E2 /* Sources */, 213 | FAEC34B61C9059DF004177E2 /* Frameworks */, 214 | FAEC34B71C9059DF004177E2 /* Resources */, 215 | ); 216 | buildRules = ( 217 | ); 218 | dependencies = ( 219 | FAEC34BC1C9059DF004177E2 /* PBXTargetDependency */, 220 | ); 221 | name = MagnetTests; 222 | productName = MagnetTests; 223 | productReference = FAEC34B91C9059DF004177E2 /* MagnetTests.xctest */; 224 | productType = "com.apple.product-type.bundle.unit-test"; 225 | }; 226 | /* End PBXNativeTarget section */ 227 | 228 | /* Begin PBXProject section */ 229 | FAEC34A61C9059DF004177E2 /* Project object */ = { 230 | isa = PBXProject; 231 | attributes = { 232 | LastSwiftUpdateCheck = 0700; 233 | LastUpgradeCheck = 1020; 234 | ORGANIZATIONNAME = "Clipy Project"; 235 | TargetAttributes = { 236 | FAEC34AE1C9059DF004177E2 = { 237 | CreatedOnToolsVersion = 7.0.1; 238 | LastSwiftMigration = 1020; 239 | }; 240 | FAEC34B81C9059DF004177E2 = { 241 | CreatedOnToolsVersion = 7.0.1; 242 | LastSwiftMigration = 1020; 243 | }; 244 | }; 245 | }; 246 | buildConfigurationList = FAEC34A91C9059DF004177E2 /* Build configuration list for PBXProject "Magnet" */; 247 | compatibilityVersion = "Xcode 3.2"; 248 | developmentRegion = en; 249 | hasScannedForEncodings = 0; 250 | knownRegions = ( 251 | en, 252 | Base, 253 | ); 254 | mainGroup = FAEC34A51C9059DF004177E2; 255 | productRefGroup = FAEC34B01C9059DF004177E2 /* Products */; 256 | projectDirPath = ""; 257 | projectReferences = ( 258 | { 259 | ProductGroup = 091D85CA2459553A00930473 /* Products */; 260 | ProjectRef = 091D85C92459553A00930473 /* Sauce.xcodeproj */; 261 | }, 262 | ); 263 | projectRoot = ""; 264 | targets = ( 265 | FAEC34AE1C9059DF004177E2 /* Magnet */, 266 | FAEC34B81C9059DF004177E2 /* MagnetTests */, 267 | ); 268 | }; 269 | /* End PBXProject section */ 270 | 271 | /* Begin PBXReferenceProxy section */ 272 | 091D85CF2459553A00930473 /* Sauce.framework */ = { 273 | isa = PBXReferenceProxy; 274 | fileType = wrapper.framework; 275 | path = Sauce.framework; 276 | remoteRef = 091D85CE2459553A00930473 /* PBXContainerItemProxy */; 277 | sourceTree = BUILT_PRODUCTS_DIR; 278 | }; 279 | 091D85D12459553A00930473 /* SauceTests.xctest */ = { 280 | isa = PBXReferenceProxy; 281 | fileType = wrapper.cfbundle; 282 | path = SauceTests.xctest; 283 | remoteRef = 091D85D02459553A00930473 /* PBXContainerItemProxy */; 284 | sourceTree = BUILT_PRODUCTS_DIR; 285 | }; 286 | /* End PBXReferenceProxy section */ 287 | 288 | /* Begin PBXResourcesBuildPhase section */ 289 | FAEC34AD1C9059DF004177E2 /* Resources */ = { 290 | isa = PBXResourcesBuildPhase; 291 | buildActionMask = 2147483647; 292 | files = ( 293 | ); 294 | runOnlyForDeploymentPostprocessing = 0; 295 | }; 296 | FAEC34B71C9059DF004177E2 /* Resources */ = { 297 | isa = PBXResourcesBuildPhase; 298 | buildActionMask = 2147483647; 299 | files = ( 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | }; 303 | /* End PBXResourcesBuildPhase section */ 304 | 305 | /* Begin PBXSourcesBuildPhase section */ 306 | FAEC34AA1C9059DF004177E2 /* Sources */ = { 307 | isa = PBXSourcesBuildPhase; 308 | buildActionMask = 2147483647; 309 | files = ( 310 | FAEC34DA1C905B5A004177E2 /* KeyCombo.swift in Sources */, 311 | FAEC34DE1C905B7C004177E2 /* HotKeyCenter.swift in Sources */, 312 | 091D85EA245A917500930473 /* NSEventExtension.swift in Sources */, 313 | FAEC34D81C905B4D004177E2 /* HotKey.swift in Sources */, 314 | 099F2A02246D091400992925 /* ModifierEventHandler.swift in Sources */, 315 | 091D85EC245A917500930473 /* KeyExtension.swift in Sources */, 316 | 091D85ED245A917500930473 /* CollectionExtension.swift in Sources */, 317 | 091D85EB245A917500930473 /* IntExtension.swift in Sources */, 318 | ); 319 | runOnlyForDeploymentPostprocessing = 0; 320 | }; 321 | FAEC34B51C9059DF004177E2 /* Sources */ = { 322 | isa = PBXSourcesBuildPhase; 323 | buildActionMask = 2147483647; 324 | files = ( 325 | FADEA45A24796103003AEC83 /* v3_1_0KeyCombo.swift in Sources */, 326 | 099F2A07246D094500992925 /* ModifierEventHandlerTests.swift in Sources */, 327 | 090077D4245D449F0099B20A /* KeyComboTests.swift in Sources */, 328 | 099F29FA246CFA9500992925 /* v2_0_0KeyCombo.swift in Sources */, 329 | FA3AA2162315A6A3007EAA1F /* CollectionExtensionTests.swift in Sources */, 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | }; 333 | /* End PBXSourcesBuildPhase section */ 334 | 335 | /* Begin PBXTargetDependency section */ 336 | FAEC34BC1C9059DF004177E2 /* PBXTargetDependency */ = { 337 | isa = PBXTargetDependency; 338 | target = FAEC34AE1C9059DF004177E2 /* Magnet */; 339 | targetProxy = FAEC34BB1C9059DF004177E2 /* PBXContainerItemProxy */; 340 | }; 341 | /* End PBXTargetDependency section */ 342 | 343 | /* Begin XCBuildConfiguration section */ 344 | FAEC34C11C9059DF004177E2 /* Debug */ = { 345 | isa = XCBuildConfiguration; 346 | buildSettings = { 347 | ALWAYS_SEARCH_USER_PATHS = NO; 348 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 349 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 350 | CLANG_CXX_LIBRARY = "libc++"; 351 | CLANG_ENABLE_MODULES = YES; 352 | CLANG_ENABLE_OBJC_ARC = YES; 353 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 354 | CLANG_WARN_BOOL_CONVERSION = YES; 355 | CLANG_WARN_COMMA = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 358 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 359 | CLANG_WARN_EMPTY_BODY = YES; 360 | CLANG_WARN_ENUM_CONVERSION = YES; 361 | CLANG_WARN_INFINITE_RECURSION = YES; 362 | CLANG_WARN_INT_CONVERSION = YES; 363 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 364 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 365 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 366 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 367 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 368 | CLANG_WARN_STRICT_PROTOTYPES = YES; 369 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 370 | CLANG_WARN_UNREACHABLE_CODE = YES; 371 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 372 | COPY_PHASE_STRIP = NO; 373 | CURRENT_PROJECT_VERSION = 1; 374 | DEBUG_INFORMATION_FORMAT = dwarf; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | ENABLE_TESTABILITY = YES; 377 | GCC_C_LANGUAGE_STANDARD = gnu99; 378 | GCC_DYNAMIC_NO_PIC = NO; 379 | GCC_NO_COMMON_BLOCKS = YES; 380 | GCC_OPTIMIZATION_LEVEL = 0; 381 | GCC_PREPROCESSOR_DEFINITIONS = ( 382 | "DEBUG=1", 383 | "$(inherited)", 384 | ); 385 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 386 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 387 | GCC_WARN_UNDECLARED_SELECTOR = YES; 388 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 389 | GCC_WARN_UNUSED_FUNCTION = YES; 390 | GCC_WARN_UNUSED_VARIABLE = YES; 391 | MACOSX_DEPLOYMENT_TARGET = 10.13; 392 | MTL_ENABLE_DEBUG_INFO = YES; 393 | ONLY_ACTIVE_ARCH = YES; 394 | SDKROOT = macosx; 395 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 396 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 397 | SWIFT_VERSION = 3.0; 398 | VERSIONING_SYSTEM = "apple-generic"; 399 | VERSION_INFO_PREFIX = ""; 400 | }; 401 | name = Debug; 402 | }; 403 | FAEC34C21C9059DF004177E2 /* Release */ = { 404 | isa = XCBuildConfiguration; 405 | buildSettings = { 406 | ALWAYS_SEARCH_USER_PATHS = NO; 407 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 408 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 409 | CLANG_CXX_LIBRARY = "libc++"; 410 | CLANG_ENABLE_MODULES = YES; 411 | CLANG_ENABLE_OBJC_ARC = YES; 412 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 413 | CLANG_WARN_BOOL_CONVERSION = YES; 414 | CLANG_WARN_COMMA = YES; 415 | CLANG_WARN_CONSTANT_CONVERSION = YES; 416 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 417 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 418 | CLANG_WARN_EMPTY_BODY = YES; 419 | CLANG_WARN_ENUM_CONVERSION = YES; 420 | CLANG_WARN_INFINITE_RECURSION = YES; 421 | CLANG_WARN_INT_CONVERSION = YES; 422 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 423 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 424 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 425 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 426 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 427 | CLANG_WARN_STRICT_PROTOTYPES = YES; 428 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 429 | CLANG_WARN_UNREACHABLE_CODE = YES; 430 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 431 | COPY_PHASE_STRIP = NO; 432 | CURRENT_PROJECT_VERSION = 1; 433 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 434 | ENABLE_NS_ASSERTIONS = NO; 435 | ENABLE_STRICT_OBJC_MSGSEND = YES; 436 | GCC_C_LANGUAGE_STANDARD = gnu99; 437 | GCC_NO_COMMON_BLOCKS = YES; 438 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 439 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 440 | GCC_WARN_UNDECLARED_SELECTOR = YES; 441 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 442 | GCC_WARN_UNUSED_FUNCTION = YES; 443 | GCC_WARN_UNUSED_VARIABLE = YES; 444 | MACOSX_DEPLOYMENT_TARGET = 10.13; 445 | MTL_ENABLE_DEBUG_INFO = NO; 446 | SDKROOT = macosx; 447 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 448 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 449 | SWIFT_VERSION = 3.0; 450 | VERSIONING_SYSTEM = "apple-generic"; 451 | VERSION_INFO_PREFIX = ""; 452 | }; 453 | name = Release; 454 | }; 455 | FAEC34C41C9059DF004177E2 /* Debug */ = { 456 | isa = XCBuildConfiguration; 457 | buildSettings = { 458 | CLANG_ENABLE_MODULES = YES; 459 | COMBINE_HIDPI_IMAGES = YES; 460 | DEFINES_MODULE = YES; 461 | DYLIB_COMPATIBILITY_VERSION = 1; 462 | DYLIB_CURRENT_VERSION = 1; 463 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 464 | FRAMEWORK_VERSION = A; 465 | INFOPLIST_FILE = Magnet/Info.plist; 466 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 467 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 468 | MARKETING_VERSION = 3.4.0; 469 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Magnet"; 470 | PRODUCT_NAME = "$(TARGET_NAME)"; 471 | SKIP_INSTALL = YES; 472 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 473 | SWIFT_VERSION = 5.0; 474 | }; 475 | name = Debug; 476 | }; 477 | FAEC34C51C9059DF004177E2 /* Release */ = { 478 | isa = XCBuildConfiguration; 479 | buildSettings = { 480 | CLANG_ENABLE_MODULES = YES; 481 | COMBINE_HIDPI_IMAGES = YES; 482 | DEFINES_MODULE = YES; 483 | DYLIB_COMPATIBILITY_VERSION = 1; 484 | DYLIB_CURRENT_VERSION = 1; 485 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 486 | FRAMEWORK_VERSION = A; 487 | INFOPLIST_FILE = Magnet/Info.plist; 488 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 489 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 490 | MARKETING_VERSION = 3.4.0; 491 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.Magnet"; 492 | PRODUCT_NAME = "$(TARGET_NAME)"; 493 | SKIP_INSTALL = YES; 494 | SWIFT_VERSION = 5.0; 495 | }; 496 | name = Release; 497 | }; 498 | FAEC34C71C9059DF004177E2 /* Debug */ = { 499 | isa = XCBuildConfiguration; 500 | buildSettings = { 501 | COMBINE_HIDPI_IMAGES = YES; 502 | INFOPLIST_FILE = MagnetTests/Info.plist; 503 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 504 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.MagnetTests"; 505 | PRODUCT_NAME = "$(TARGET_NAME)"; 506 | SWIFT_VERSION = 5.0; 507 | }; 508 | name = Debug; 509 | }; 510 | FAEC34C81C9059DF004177E2 /* Release */ = { 511 | isa = XCBuildConfiguration; 512 | buildSettings = { 513 | COMBINE_HIDPI_IMAGES = YES; 514 | INFOPLIST_FILE = MagnetTests/Info.plist; 515 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 516 | PRODUCT_BUNDLE_IDENTIFIER = "com.clipy-app.MagnetTests"; 517 | PRODUCT_NAME = "$(TARGET_NAME)"; 518 | SWIFT_VERSION = 5.0; 519 | }; 520 | name = Release; 521 | }; 522 | /* End XCBuildConfiguration section */ 523 | 524 | /* Begin XCConfigurationList section */ 525 | FAEC34A91C9059DF004177E2 /* Build configuration list for PBXProject "Magnet" */ = { 526 | isa = XCConfigurationList; 527 | buildConfigurations = ( 528 | FAEC34C11C9059DF004177E2 /* Debug */, 529 | FAEC34C21C9059DF004177E2 /* Release */, 530 | ); 531 | defaultConfigurationIsVisible = 0; 532 | defaultConfigurationName = Release; 533 | }; 534 | FAEC34C31C9059DF004177E2 /* Build configuration list for PBXNativeTarget "Magnet" */ = { 535 | isa = XCConfigurationList; 536 | buildConfigurations = ( 537 | FAEC34C41C9059DF004177E2 /* Debug */, 538 | FAEC34C51C9059DF004177E2 /* Release */, 539 | ); 540 | defaultConfigurationIsVisible = 0; 541 | defaultConfigurationName = Release; 542 | }; 543 | FAEC34C61C9059DF004177E2 /* Build configuration list for PBXNativeTarget "MagnetTests" */ = { 544 | isa = XCConfigurationList; 545 | buildConfigurations = ( 546 | FAEC34C71C9059DF004177E2 /* Debug */, 547 | FAEC34C81C9059DF004177E2 /* Release */, 548 | ); 549 | defaultConfigurationIsVisible = 0; 550 | defaultConfigurationName = Release; 551 | }; 552 | /* End XCConfigurationList section */ 553 | }; 554 | rootObject = FAEC34A61C9059DF004177E2 /* Project object */; 555 | } 556 | -------------------------------------------------------------------------------- /Lib/Magnet.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Lib/Magnet.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | 7 | // ___FILENAME___ 8 | // 9 | // ___PACKAGENAME___ 10 | // GitHub: ___REPOSITORYURL___ 11 | // HP: ___PRODUCTURL___ 12 | // 13 | // Copyright © 2015-___YEAR___ Clipy Project. 14 | // 15 | REPOSITORYURL 16 | https://github.com/clipy 17 | PRODUCTURL 18 | https://clipy-app.com 19 | 20 | 21 | -------------------------------------------------------------------------------- /Lib/Magnet.xcodeproj/xcshareddata/xcschemes/Magnet.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /Lib/Magnet/Extensions/CollectionExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionExtension.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Foundation 12 | 13 | public extension Collection where Element == Bool { 14 | var trueCount: Int { 15 | return filter { $0 }.count 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Lib/Magnet/Extensions/IntExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // IntExtension.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | import Carbon 13 | 14 | public extension Int { 15 | @available(*, deprecated, renamed: "NSEvent.ModifierFlags.init(carbonModifiers:)") 16 | func convertSupportCococaModifiers() -> NSEvent.ModifierFlags { 17 | return convertSupportCocoaModifiers() 18 | } 19 | 20 | @available(*, deprecated, renamed: "NSEvent.ModifierFlags.init(carbonModifiers:)") 21 | func convertSupportCocoaModifiers() -> NSEvent.ModifierFlags { 22 | return NSEvent.ModifierFlags(carbonModifiers: self) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Lib/Magnet/Extensions/KeyExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyExtension.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Foundation 12 | import Sauce 13 | 14 | public extension Key { 15 | var isFunctionKey: Bool { 16 | switch self { 17 | case .f1, 18 | .f2, 19 | .f3, 20 | .f4, 21 | .f5, 22 | .f6, 23 | .f7, 24 | .f8, 25 | .f9, 26 | .f10, 27 | .f11, 28 | .f12, 29 | .f13, 30 | .f14, 31 | .f15, 32 | .f16, 33 | .f17, 34 | .f18, 35 | .f19, 36 | .f20: 37 | return true 38 | default: 39 | return false 40 | } 41 | } 42 | var isAlphabet: Bool { 43 | switch self { 44 | case .a, 45 | .b, 46 | .c, 47 | .d, 48 | .e, 49 | .f, 50 | .g, 51 | .h, 52 | .i, 53 | .j, 54 | .k, 55 | .l, 56 | .m, 57 | .n, 58 | .o, 59 | .p, 60 | .q, 61 | .r, 62 | .s, 63 | .t, 64 | .u, 65 | .v, 66 | .w, 67 | .x, 68 | .y, 69 | .z: 70 | return true 71 | default: 72 | return false 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Lib/Magnet/Extensions/NSEventExtension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSEventExtension.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | import Carbon 13 | import Sauce 14 | 15 | public extension NSEvent.ModifierFlags { 16 | var containsSupportModifiers: Bool { 17 | return contains(.command) || contains(.option) || contains(.control) || contains(.shift) || contains(.function) 18 | } 19 | 20 | var isSingleFlags: Bool { 21 | let commandSelected = contains(.command) 22 | let optionSelected = contains(.option) 23 | let controlSelected = contains(.control) 24 | let shiftSelected = contains(.shift) 25 | return [commandSelected, optionSelected, controlSelected, shiftSelected].trueCount == 1 26 | } 27 | 28 | func filterUnsupportModifiers() -> NSEvent.ModifierFlags { 29 | var filterdModifierFlags = NSEvent.ModifierFlags(rawValue: 0) 30 | if contains(.command) { 31 | filterdModifierFlags.insert(.command) 32 | } 33 | if contains(.option) { 34 | filterdModifierFlags.insert(.option) 35 | } 36 | if contains(.control) { 37 | filterdModifierFlags.insert(.control) 38 | } 39 | if contains(.shift) { 40 | filterdModifierFlags.insert(.shift) 41 | } 42 | return filterdModifierFlags 43 | } 44 | 45 | func filterNotShiftModifiers() -> NSEvent.ModifierFlags { 46 | guard contains(.shift) else { return NSEvent.ModifierFlags(rawValue: 0) } 47 | return .shift 48 | } 49 | 50 | func keyEquivalentStrings() -> [String] { 51 | var strings = [String]() 52 | if contains(.control) { 53 | strings.append("⌃") 54 | } 55 | if contains(.option) { 56 | strings.append("⌥") 57 | } 58 | if contains(.shift) { 59 | strings.append("⇧") 60 | } 61 | if contains(.command) { 62 | strings.append("⌘") 63 | } 64 | return strings 65 | } 66 | } 67 | 68 | public extension NSEvent.ModifierFlags { 69 | init(carbonModifiers: Int) { 70 | var result = NSEvent.ModifierFlags(rawValue: 0) 71 | if (carbonModifiers & cmdKey) != 0 { 72 | result.insert(.command) 73 | } 74 | if (carbonModifiers & optionKey) != 0 { 75 | result.insert(.option) 76 | } 77 | if (carbonModifiers & controlKey) != 0 { 78 | result.insert(.control) 79 | } 80 | if (carbonModifiers & shiftKey) != 0 { 81 | result.insert(.shift) 82 | } 83 | self = result 84 | } 85 | 86 | func carbonModifiers(isSupportFunctionKey: Bool = false) -> Int { 87 | var carbonModifiers: Int = 0 88 | if contains(.command) { 89 | carbonModifiers |= cmdKey 90 | } 91 | if contains(.option) { 92 | carbonModifiers |= optionKey 93 | } 94 | if contains(.control) { 95 | carbonModifiers |= controlKey 96 | } 97 | if contains(.shift) { 98 | carbonModifiers |= shiftKey 99 | } 100 | if contains(.function) && isSupportFunctionKey { 101 | carbonModifiers |= Int(NSEvent.ModifierFlags.function.rawValue) 102 | } 103 | return carbonModifiers 104 | } 105 | } 106 | 107 | extension NSEvent.EventType { 108 | fileprivate var isKeyboardEvent: Bool { 109 | return [.keyUp, .keyDown, .flagsChanged].contains(self) 110 | } 111 | } 112 | 113 | extension NSEvent { 114 | /// Returns a matching `KeyCombo` for the event, if the event is a keyboard event and the key is recognized. 115 | public var keyCombo: KeyCombo? { 116 | guard self.type.isKeyboardEvent else { return nil } 117 | guard let key = Sauce.shared.key(for: Int(self.keyCode)) else { return nil } 118 | return KeyCombo(key: key, cocoaModifiers: self.modifierFlags) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Lib/Magnet/HotKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotKey.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | import Carbon 13 | 14 | open class HotKey: NSObject { 15 | 16 | // MARK: - Properties 17 | public let identifier: String 18 | public let keyCombo: KeyCombo 19 | public let callback: ((HotKey) -> Void)? 20 | public let target: AnyObject? 21 | public let action: Selector? 22 | public let actionQueue: ActionQueue 23 | 24 | var hotKeyId: UInt32? 25 | var hotKeyRef: EventHotKeyRef? 26 | 27 | // MARK: - Enum Value 28 | public enum ActionQueue { 29 | case main 30 | case session 31 | 32 | public func execute(closure: @escaping () -> Void) { 33 | switch self { 34 | case .main: 35 | DispatchQueue.main.async { 36 | closure() 37 | } 38 | case .session: 39 | closure() 40 | } 41 | } 42 | } 43 | 44 | // MARK: - Initialize 45 | public init(identifier: String, keyCombo: KeyCombo, target: AnyObject, action: Selector, actionQueue: ActionQueue = .main) { 46 | self.identifier = identifier 47 | self.keyCombo = keyCombo 48 | self.callback = nil 49 | self.target = target 50 | self.action = action 51 | self.actionQueue = actionQueue 52 | super.init() 53 | } 54 | 55 | public init(identifier: String, keyCombo: KeyCombo, actionQueue: ActionQueue = .main, handler: @escaping ((HotKey) -> Void)) { 56 | self.identifier = identifier 57 | self.keyCombo = keyCombo 58 | self.callback = handler 59 | self.target = nil 60 | self.action = nil 61 | self.actionQueue = actionQueue 62 | super.init() 63 | } 64 | 65 | } 66 | 67 | // MARK: - Invoke 68 | extension HotKey { 69 | public func invoke() { 70 | guard let callback = self.callback else { 71 | guard let target = self.target as? NSObject, let selector = self.action else { return } 72 | guard target.responds(to: selector) else { return } 73 | actionQueue.execute { [weak self] in 74 | guard let wSelf = self else { return } 75 | target.perform(selector, with: wSelf) 76 | } 77 | return 78 | } 79 | actionQueue.execute { [weak self] in 80 | guard let wSelf = self else { return } 81 | callback(wSelf) 82 | } 83 | } 84 | } 85 | 86 | // MARK: - Register & UnRegister 87 | extension HotKey { 88 | @discardableResult 89 | public func register() -> Bool { 90 | return HotKeyCenter.shared.register(with: self) 91 | } 92 | 93 | public func unregister() { 94 | return HotKeyCenter.shared.unregister(with: self) 95 | } 96 | } 97 | 98 | // MARK: - override isEqual 99 | extension HotKey { 100 | override public func isEqual(_ object: Any?) -> Bool { 101 | guard let hotKey = object as? HotKey else { return false } 102 | 103 | return self.identifier == hotKey.identifier && 104 | self.keyCombo == hotKey.keyCombo && 105 | self.hotKeyId == hotKey.hotKeyId && 106 | self.hotKeyRef == hotKey.hotKeyRef 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Lib/Magnet/HotKeyCenter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotKeyCenter.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | import Carbon 13 | 14 | open class HotKeyCenter { 15 | 16 | // MARK: - Properties 17 | public static let shared = HotKeyCenter() 18 | 19 | private var hotKeys = [String: HotKey]() 20 | private var hotKeyCount: UInt32 = 0 21 | private let modifierEventHandler: ModifierEventHandler 22 | private let notificationCenter: NotificationCenter 23 | 24 | // MARK: - Initialize 25 | init(modifierEventHandler: ModifierEventHandler = .init(), notificationCenter: NotificationCenter = .default) { 26 | self.modifierEventHandler = modifierEventHandler 27 | self.notificationCenter = notificationCenter 28 | installHotKeyPressedEventHandler() 29 | installModifiersChangedEventHandlerIfNeeded() 30 | observeApplicationTerminate() 31 | } 32 | 33 | deinit { 34 | notificationCenter.removeObserver(self) 35 | } 36 | 37 | } 38 | 39 | // MARK: - Register & Unregister 40 | extension HotKeyCenter { 41 | @discardableResult 42 | public func register(with hotKey: HotKey) -> Bool { 43 | guard !hotKeys.keys.contains(hotKey.identifier) else { return false } 44 | guard !hotKeys.values.contains(hotKey) else { return false } 45 | 46 | hotKeys[hotKey.identifier] = hotKey 47 | guard !hotKey.keyCombo.doubledModifiers else { return true } 48 | /* 49 | * Normal macOS shortcut 50 | * 51 | * Discussion: 52 | * When registering a hotkey, a KeyCode that conforms to the 53 | * keyboard layout at the time of registration is registered. 54 | * To register a `v` on the QWERTY keyboard, `9` is registered, 55 | * and to register a `v` on the Dvorak keyboard, `47` is registered. 56 | * Therefore, if you change the keyboard layout after registering 57 | * a hot key, the hot key is not assigned to the correct key. 58 | * To solve this problem, you need to re-register the hotkeys 59 | * when you change the layout, but it's not supported by the 60 | * Apple Genuine app either, so it's not supported now. 61 | */ 62 | let hotKeyId = EventHotKeyID(signature: UTGetOSTypeFromString("Magnet" as CFString), id: hotKeyCount) 63 | var carbonHotKey: EventHotKeyRef? 64 | let error = RegisterEventHotKey(UInt32(hotKey.keyCombo.currentKeyCode), 65 | UInt32(hotKey.keyCombo.modifiers), 66 | hotKeyId, 67 | GetEventDispatcherTarget(), 68 | 0, 69 | &carbonHotKey) 70 | guard error == noErr else { 71 | unregister(with: hotKey) 72 | return false 73 | } 74 | hotKey.hotKeyId = hotKeyId.id 75 | hotKey.hotKeyRef = carbonHotKey 76 | hotKeyCount += 1 77 | 78 | return true 79 | } 80 | 81 | public func unregister(with hotKey: HotKey) { 82 | if let carbonHotKey = hotKey.hotKeyRef { 83 | UnregisterEventHotKey(carbonHotKey) 84 | } 85 | hotKeys.removeValue(forKey: hotKey.identifier) 86 | hotKey.hotKeyId = nil 87 | hotKey.hotKeyRef = nil 88 | } 89 | 90 | @discardableResult 91 | public func unregisterHotKey(with identifier: String) -> Bool { 92 | guard let hotKey = hotKeys[identifier] else { return false } 93 | unregister(with: hotKey) 94 | return true 95 | } 96 | 97 | public func unregisterAll() { 98 | hotKeys.forEach { unregister(with: $1) } 99 | } 100 | } 101 | 102 | // MARK: - Terminate 103 | extension HotKeyCenter { 104 | private func observeApplicationTerminate() { 105 | notificationCenter.addObserver(self, 106 | selector: #selector(HotKeyCenter.applicationWillTerminate), 107 | name: NSApplication.willTerminateNotification, 108 | object: nil) 109 | } 110 | 111 | @objc func applicationWillTerminate() { 112 | unregisterAll() 113 | } 114 | } 115 | 116 | // MARK: - HotKey Events 117 | extension HotKeyCenter { 118 | public func installHotKeyPressedEventHandler() { 119 | var pressedEventType = EventTypeSpec() 120 | pressedEventType.eventClass = OSType(kEventClassKeyboard) 121 | pressedEventType.eventKind = OSType(kEventHotKeyPressed) 122 | InstallEventHandler(GetEventDispatcherTarget(), { _, inEvent, _ -> OSStatus in 123 | return HotKeyCenter.shared.sendPressedKeyboardEvent(inEvent!) 124 | }, 1, &pressedEventType, nil, nil) 125 | } 126 | 127 | public func sendPressedKeyboardEvent(_ event: EventRef) -> OSStatus { 128 | assert(Int(GetEventClass(event)) == kEventClassKeyboard, "Unknown event class") 129 | 130 | var hotKeyId = EventHotKeyID() 131 | let error = GetEventParameter(event, 132 | EventParamName(kEventParamDirectObject), 133 | EventParamName(typeEventHotKeyID), 134 | nil, 135 | MemoryLayout.size, 136 | nil, 137 | &hotKeyId) 138 | 139 | guard error == noErr else { return error } 140 | assert(hotKeyId.signature == UTGetOSTypeFromString("Magnet" as CFString), "Invalid hot key id") 141 | 142 | let hotKey = hotKeys.values.first(where: { $0.hotKeyId == hotKeyId.id }) 143 | switch GetEventKind(event) { 144 | case EventParamName(kEventHotKeyPressed): 145 | hotKey?.invoke() 146 | default: 147 | assert(false, "Unknown event kind") 148 | } 149 | return noErr 150 | } 151 | } 152 | 153 | // MARK: - Double Tap Modifier Event 154 | extension HotKeyCenter { 155 | public func installModifiersChangedEventHandlerIfNeeded() { 156 | NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { [weak self] event in 157 | self?.modifierEventHandler.handleModifiersEvent(with: event.modifierFlags, timestamp: event.timestamp) 158 | } 159 | NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { [weak self] event -> NSEvent? in 160 | self?.modifierEventHandler.handleModifiersEvent(with: event.modifierFlags, timestamp: event.timestamp) 161 | return event 162 | } 163 | modifierEventHandler.doubleTapped = { [weak self] tappedModifierFlags in 164 | self?.hotKeys.values 165 | .filter { $0.keyCombo.doubledModifiers } 166 | .filter { $0.keyCombo.modifiers == tappedModifierFlags.carbonModifiers() } 167 | .forEach { $0.invoke() } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /Lib/Magnet/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 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright © 2016年 Shunsuke Furubayashi. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Lib/Magnet/KeyCombo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyCombo.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | import Carbon 13 | import Sauce 14 | 15 | open class KeyCombo: NSObject, NSCopying, NSCoding, Codable { 16 | 17 | // MARK: - Properties 18 | public let key: Key 19 | public let modifiers: Int 20 | public let doubledModifiers: Bool 21 | public var QWERTYKeyCode: Int { 22 | guard !doubledModifiers else { return 0 } 23 | return Int(key.QWERTYKeyCode) 24 | } 25 | public var characters: String { 26 | guard !doubledModifiers else { return "" } 27 | return Sauce.shared.character(for: Int(Sauce.shared.keyCode(for: key)), carbonModifiers: modifiers) ?? "" 28 | } 29 | public var keyEquivalent: String { 30 | guard !doubledModifiers else { return "" } 31 | let keyCode = Int(Sauce.shared.keyCode(for: key)) 32 | guard key.isAlphabet else { return Sauce.shared.character(for: keyCode, cocoaModifiers: []) ?? "" } 33 | let modifiers = keyEquivalentModifierMask.filterNotShiftModifiers() 34 | return Sauce.shared.character(for: keyCode, cocoaModifiers: modifiers) ?? "" 35 | } 36 | public var keyEquivalentModifierMask: NSEvent.ModifierFlags { 37 | return NSEvent.ModifierFlags(carbonModifiers: self.modifiers) 38 | } 39 | public var keyEquivalentModifierMaskString: String { 40 | return keyEquivalentModifierMask.keyEquivalentStrings().joined() 41 | } 42 | public var currentKeyCode: CGKeyCode { 43 | guard !doubledModifiers else { return 0 } 44 | return Sauce.shared.keyCode(for: key) 45 | } 46 | 47 | // MARK: - Initialize 48 | public convenience init?(QWERTYKeyCode: Int, carbonModifiers: Int) { 49 | self.init(QWERTYKeyCode: QWERTYKeyCode, cocoaModifiers: NSEvent.ModifierFlags(carbonModifiers: carbonModifiers)) 50 | } 51 | 52 | public convenience init?(QWERTYKeyCode: Int, cocoaModifiers: NSEvent.ModifierFlags) { 53 | guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { return nil } 54 | self.init(key: key, cocoaModifiers: cocoaModifiers) 55 | } 56 | 57 | public convenience init?(key: Key, carbonModifiers: Int) { 58 | self.init(key: key, cocoaModifiers: NSEvent.ModifierFlags(carbonModifiers: carbonModifiers)) 59 | } 60 | 61 | public init?(key: Key, cocoaModifiers: NSEvent.ModifierFlags) { 62 | var filterdCocoaModifiers = cocoaModifiers.filterUnsupportModifiers() 63 | // In the case of the function key, will need to add the modifier manually 64 | if key.isFunctionKey { 65 | filterdCocoaModifiers.insert(.function) 66 | } 67 | guard filterdCocoaModifiers.containsSupportModifiers else { return nil } 68 | self.key = key 69 | self.modifiers = filterdCocoaModifiers.carbonModifiers(isSupportFunctionKey: true) 70 | self.doubledModifiers = false 71 | } 72 | 73 | public convenience init?(doubledCarbonModifiers modifiers: Int) { 74 | self.init(doubledCocoaModifiers: NSEvent.ModifierFlags(carbonModifiers: modifiers)) 75 | } 76 | 77 | public init?(doubledCocoaModifiers modifiers: NSEvent.ModifierFlags) { 78 | let filterdCocoaModifiers = modifiers.filterUnsupportModifiers() 79 | guard modifiers.isSingleFlags else { return nil } 80 | self.key = .a 81 | self.modifiers = filterdCocoaModifiers.carbonModifiers() 82 | self.doubledModifiers = true 83 | } 84 | 85 | // MARK: - NSCoping 86 | public func copy(with zone: NSZone?) -> Any { 87 | if doubledModifiers { 88 | return KeyCombo(doubledCarbonModifiers: modifiers) as Any 89 | } else { 90 | return KeyCombo(key: key, carbonModifiers: modifiers) as Any 91 | } 92 | } 93 | 94 | // MARK: - NSCoding 95 | public required init?(coder aDecoder: NSCoder) { 96 | self.doubledModifiers = aDecoder.decodeBool(forKey: CodingKeys.doubledModifiers.rawValue) 97 | self.modifiers = aDecoder.decodeInteger(forKey: CodingKeys.modifiers.rawValue) 98 | guard !doubledModifiers else { 99 | self.key = .a 100 | return 101 | } 102 | // Changed KeyCode to Key from v3.2.0 103 | guard !aDecoder.containsValue(forKey: CodingKeys.key.rawValue) else { 104 | guard let keyRawValue = aDecoder.decodeObject(forKey: CodingKeys.key.rawValue) as? String else { return nil } 105 | guard let key = Key(rawValue: keyRawValue) else { return nil } 106 | self.key = key 107 | return 108 | } 109 | // Changed KeyCode to QWERTYKeyCode from v3.0.0 110 | let QWERTYKeyCode: Int 111 | if aDecoder.containsValue(forKey: CodingKeys.keyCode.rawValue) { 112 | QWERTYKeyCode = aDecoder.decodeInteger(forKey: CodingKeys.keyCode.rawValue) 113 | } else { 114 | QWERTYKeyCode = aDecoder.decodeInteger(forKey: CodingKeys.QWERTYKeyCode.rawValue) 115 | } 116 | guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { return nil } 117 | self.key = key 118 | } 119 | 120 | public func encode(with aCoder: NSCoder) { 121 | aCoder.encode(key.rawValue, forKey: CodingKeys.key.rawValue) 122 | aCoder.encode(modifiers, forKey: CodingKeys.modifiers.rawValue) 123 | aCoder.encode(doubledModifiers, forKey: CodingKeys.doubledModifiers.rawValue) 124 | } 125 | 126 | // MARK: - Codable 127 | public required init(from decoder: Decoder) throws { 128 | let container = try decoder.container(keyedBy: CodingKeys.self) 129 | self.doubledModifiers = try container.decode(Bool.self, forKey: .doubledModifiers) 130 | self.modifiers = try container.decode(Int.self, forKey: .modifiers) 131 | guard !doubledModifiers else { 132 | self.key = .a 133 | return 134 | } 135 | // Changed KeyCode to Key from v3.2.0 136 | guard !container.contains(.key) else { 137 | self.key = try container.decode(Key.self, forKey: .key) 138 | return 139 | } 140 | // Changed KeyCode to QWERTYKeyCode from v3.0.0 141 | let QWERTYKeyCode: Int 142 | if container.contains(.keyCode) { 143 | QWERTYKeyCode = try container.decode(Int.self, forKey: .keyCode) 144 | } else { 145 | QWERTYKeyCode = try container.decode(Int.self, forKey: .QWERTYKeyCode) 146 | } 147 | guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { throw KeyCombo.InitializeError() } 148 | self.key = key 149 | } 150 | 151 | public func encode(to encoder: Encoder) throws { 152 | var container = encoder.container(keyedBy: CodingKeys.self) 153 | try container.encode(key, forKey: .key) 154 | try container.encode(modifiers, forKey: .modifiers) 155 | try container.encode(doubledModifiers, forKey: .doubledModifiers) 156 | } 157 | 158 | // MARK: - Coding Keys 159 | private enum CodingKeys: String, CodingKey { 160 | case key 161 | case keyCode 162 | case QWERTYKeyCode 163 | case modifiers 164 | case doubledModifiers 165 | } 166 | 167 | // MARK: - Equatable 168 | public override func isEqual(_ object: Any?) -> Bool { 169 | guard let keyCombo = object as? KeyCombo else { return false } 170 | return key == keyCombo.key && 171 | modifiers == keyCombo.modifiers && 172 | doubledModifiers == keyCombo.doubledModifiers 173 | } 174 | 175 | } 176 | 177 | // MARK: - Error 178 | extension KeyCombo { 179 | public struct InitializeError: Error {} 180 | } 181 | -------------------------------------------------------------------------------- /Lib/Magnet/Magnet.h: -------------------------------------------------------------------------------- 1 | // 2 | // Magnet.h 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | #import 12 | 13 | //! Project version number for Magnet. 14 | FOUNDATION_EXPORT double MagnetVersionNumber; 15 | 16 | //! Project version string for Magnet. 17 | FOUNDATION_EXPORT const unsigned char MagnetVersionString[]; 18 | 19 | // In this header, you should import all the public headers of your framework using statements like #import 20 | 21 | 22 | -------------------------------------------------------------------------------- /Lib/Magnet/ModifierEventHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifierEventHandler.swift 3 | // 4 | // Magnet 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | 13 | open class ModifierEventHandler { 14 | 15 | // MARK: - Properties 16 | public var doubleTapped: ((NSEvent.ModifierFlags) -> Void)? 17 | 18 | private var tappingModifierFlags = NSEvent.ModifierFlags() 19 | private var isTappingMultiModifiers = false 20 | private var lastHandledEventTimestamp: TimeInterval? 21 | private let cleanTimeInterval: DispatchTimeInterval 22 | private let cleanQueue: DispatchQueue 23 | 24 | // MARK: - Initialize 25 | public init(cleanTimeInterval: DispatchTimeInterval = .milliseconds(300), cleanQueue: DispatchQueue = .main) { 26 | self.cleanTimeInterval = cleanTimeInterval 27 | self.cleanQueue = cleanQueue 28 | } 29 | 30 | } 31 | 32 | // MARK: - Handling 33 | extension ModifierEventHandler { 34 | public func handleModifiersEvent(with modifierFlags: NSEvent.ModifierFlags, timestamp: TimeInterval) { 35 | guard lastHandledEventTimestamp != timestamp else { return } 36 | lastHandledEventTimestamp = timestamp 37 | 38 | handleDoubleTapModifierEvent(modifierFlags: modifierFlags) 39 | } 40 | 41 | private func handleDoubleTapModifierEvent(modifierFlags: NSEvent.ModifierFlags) { 42 | let tappedModifierFlags = modifierFlags.filterUnsupportModifiers() 43 | let commandTapped = tappedModifierFlags.contains(.command) 44 | let shiftTapped = tappedModifierFlags.contains(.shift) 45 | let controlTapped = tappedModifierFlags.contains(.control) 46 | let optionTapped = tappedModifierFlags.contains(.option) 47 | let tappedModifierCount = [commandTapped, shiftTapped, controlTapped, optionTapped].trueCount 48 | guard tappedModifierCount != 0 else { return } 49 | guard tappedModifierCount == 1 else { 50 | isTappingMultiModifiers = true 51 | return 52 | } 53 | guard !isTappingMultiModifiers else { 54 | isTappingMultiModifiers = false 55 | return 56 | } 57 | if (tappingModifierFlags.contains(.command) && commandTapped) || 58 | (tappingModifierFlags.contains(.shift) && shiftTapped) || 59 | (tappingModifierFlags.contains(.control) && controlTapped) || 60 | (tappingModifierFlags.contains(.option) && optionTapped) { 61 | doubleTapped?(tappingModifierFlags) 62 | tappingModifierFlags = NSEvent.ModifierFlags() 63 | } else { 64 | tappingModifierFlags = tappedModifierFlags 65 | } 66 | 67 | // After a certain amount of time, the tapped modifier will be reset. 68 | cleanQueue.asyncAfter(deadline: .now() + cleanTimeInterval) { [weak self] in 69 | self?.tappingModifierFlags = NSEvent.ModifierFlags() 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Lib/MagnetTests/CollectionExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionExtensionTests.swift 3 | // 4 | // MagnetTests 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import XCTest 12 | @testable import Magnet 13 | 14 | final class CollectionExtensionTests: XCTestCase { 15 | 16 | // MARK: - Tests 17 | func testTrueCount() { 18 | XCTAssertEqual([Bool]().trueCount, 0) 19 | XCTAssertEqual([true].trueCount, 1) 20 | XCTAssertEqual([false].trueCount, 0) 21 | XCTAssertEqual([true, true].trueCount, 2) 22 | XCTAssertEqual([true, false].trueCount, 1) 23 | XCTAssertEqual([false, false].trueCount, 0) 24 | XCTAssertEqual([false, true, false, true].trueCount, 2) 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Lib/MagnetTests/Fixtures/v2_0_0KeyCombo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // v2_0_0KeyCombo.swift 3 | // 4 | // MagnetTests 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Cocoa 12 | 13 | // swiftlint:disable type_name 14 | // ref: https://github.com/Clipy/Magnet/blob/v2.3.1/Lib/Magnet/KeyCombo.swift 15 | final class v2_0_0KeyCombo: NSObject, NSCoding, Codable { 16 | 17 | // MARK: - Properties 18 | public let keyCode: Int 19 | public let modifiers: Int 20 | public let doubledModifiers: Bool 21 | 22 | // MARK: - Initialize 23 | init(keyCode: Int, modifiers: Int, doubledModifiers: Bool) { 24 | self.keyCode = keyCode 25 | self.modifiers = modifiers 26 | self.doubledModifiers = doubledModifiers 27 | } 28 | 29 | public init?(coder aDecoder: NSCoder) { 30 | self.keyCode = aDecoder.decodeInteger(forKey: "keyCode") 31 | self.modifiers = aDecoder.decodeInteger(forKey: "modifiers") 32 | self.doubledModifiers = aDecoder.decodeBool(forKey: "doubledModifiers") 33 | } 34 | 35 | public func encode(with aCoder: NSCoder) { 36 | aCoder.encode(keyCode, forKey: "keyCode") 37 | aCoder.encode(modifiers, forKey: "modifiers") 38 | aCoder.encode(doubledModifiers, forKey: "doubledModifiers") 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /Lib/MagnetTests/Fixtures/v3_1_0KeyCombo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // v3_1_0KeyCombo.swift 3 | // 4 | // MagnetTests 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import Foundation 12 | import Sauce 13 | 14 | // swiftlint:disable type_name 15 | // ref: https://github.com/Clipy/Magnet/blob/v3.1.0/Lib/Magnet/KeyCombo.swift 16 | final class v3_1_0KeyCombo: NSObject, NSCoding, Codable { 17 | 18 | // MARK: - Properties 19 | public let key: Key 20 | public let modifiers: Int 21 | public let doubledModifiers: Bool 22 | public var QWERTYKeyCode: Int { 23 | guard !doubledModifiers else { return 0 } 24 | return Int(key.QWERTYKeyCode) 25 | } 26 | 27 | // MARK: - Initialize 28 | init(key: Key, modifiers: Int, doubledModifiers: Bool) { 29 | self.key = key 30 | self.modifiers = modifiers 31 | self.doubledModifiers = doubledModifiers 32 | } 33 | 34 | public init?(coder aDecoder: NSCoder) { 35 | self.doubledModifiers = aDecoder.decodeBool(forKey: CodingKeys.doubledModifiers.rawValue) 36 | if doubledModifiers { 37 | self.key = .a 38 | } else { 39 | let QWERTYKeyCode = aDecoder.decodeInteger(forKey: CodingKeys.QWERTYKeyCode.rawValue) 40 | guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { return nil } 41 | self.key = key 42 | } 43 | self.modifiers = aDecoder.decodeInteger(forKey: CodingKeys.modifiers.rawValue) 44 | } 45 | 46 | public func encode(with aCoder: NSCoder) { 47 | aCoder.encode(QWERTYKeyCode, forKey: CodingKeys.QWERTYKeyCode.rawValue) 48 | aCoder.encode(modifiers, forKey: CodingKeys.modifiers.rawValue) 49 | aCoder.encode(doubledModifiers, forKey: CodingKeys.doubledModifiers.rawValue) 50 | } 51 | 52 | public init(from decoder: Decoder) throws { 53 | let container = try decoder.container(keyedBy: CodingKeys.self) 54 | self.doubledModifiers = try container.decode(Bool.self, forKey: .doubledModifiers) 55 | if doubledModifiers { 56 | self.key = .a 57 | } else { 58 | let QWERTYKeyCode = try container.decode(Int.self, forKey: .QWERTYKeyCode) 59 | guard let key = Key(QWERTYKeyCode: QWERTYKeyCode) else { throw NSError() } 60 | self.key = key 61 | } 62 | self.modifiers = try container.decode(Int.self, forKey: .modifiers) 63 | } 64 | 65 | public func encode(to encoder: Encoder) throws { 66 | var container = encoder.container(keyedBy: CodingKeys.self) 67 | try container.encode(QWERTYKeyCode, forKey: .QWERTYKeyCode) 68 | try container.encode(modifiers, forKey: .modifiers) 69 | try container.encode(doubledModifiers, forKey: .doubledModifiers) 70 | } 71 | 72 | // MARK: - Coding Keys 73 | private enum CodingKeys: String, CodingKey { 74 | case QWERTYKeyCode 75 | case modifiers 76 | case doubledModifiers 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /Lib/MagnetTests/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 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Lib/MagnetTests/KeyComboTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyComboTests.swift 3 | // 4 | // MagnetTests 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import XCTest 12 | import Sauce 13 | import Carbon 14 | @testable import Magnet 15 | 16 | final class KeyComboTests: XCTestCase { 17 | 18 | // MARK: - Tests 19 | func testFunctionInitializer() { 20 | var keyCombo: KeyCombo? 21 | // F1 22 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: []) 23 | XCTAssertNotNil(keyCombo) 24 | // Shift + Control + Comman + Option + F1 25 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: [.shift, .control, .command, .option]) 26 | XCTAssertNotNil(keyCombo) 27 | } 28 | 29 | func testDoubledTapKeyComboInitializer() { 30 | var keyCombo: KeyCombo? 31 | // Shift double tap 32 | keyCombo = KeyCombo(doubledCocoaModifiers: .shift) 33 | XCTAssertNotNil(keyCombo) 34 | // Empty double tap 35 | keyCombo = KeyCombo(doubledCocoaModifiers: []) 36 | XCTAssertNil(keyCombo) 37 | // Shift + Control double tap 38 | keyCombo = KeyCombo(doubledCocoaModifiers: [.shift, .command]) 39 | XCTAssertNil(keyCombo) 40 | // Function double tap 41 | keyCombo = KeyCombo(doubledCocoaModifiers: [.function]) 42 | XCTAssertNil(keyCombo) 43 | } 44 | 45 | func testDoubledTapKeyComboCharacter() { 46 | var keyCombo: KeyCombo? 47 | // Shift double tap 48 | keyCombo = KeyCombo(doubledCocoaModifiers: .shift) 49 | XCTAssertEqual(keyCombo?.characters, "") 50 | // Control double tap 51 | keyCombo = KeyCombo(doubledCocoaModifiers: .control) 52 | XCTAssertEqual(keyCombo?.characters, "") 53 | // Command double tap 54 | keyCombo = KeyCombo(doubledCocoaModifiers: .command) 55 | XCTAssertEqual(keyCombo?.characters, "") 56 | // Option double tap 57 | keyCombo = KeyCombo(doubledCocoaModifiers: .option) 58 | XCTAssertEqual(keyCombo?.characters, "") 59 | } 60 | 61 | func testCharacter() { 62 | var keyCombo: KeyCombo? 63 | // Command + a 64 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.command]) 65 | XCTAssertEqual(keyCombo?.characters, "a") 66 | // Shift + a 67 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.shift]) 68 | XCTAssertEqual(keyCombo?.characters, "A") 69 | // Option + a 70 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.option]) 71 | XCTAssertEqual(keyCombo?.characters, "å") 72 | // Option + Shift + a 73 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.option, .shift]) 74 | XCTAssertEqual(keyCombo?.characters, "Å") 75 | // Option + Shift + 1 76 | keyCombo = KeyCombo(key: .one, cocoaModifiers: [.option, .shift]) 77 | XCTAssertEqual(keyCombo?.characters, "⁄") 78 | // Option + Shift + KeyPad 1 79 | keyCombo = KeyCombo(key: .keypadOne, cocoaModifiers: [.option, .shift]) 80 | XCTAssertEqual(keyCombo?.characters, "1") 81 | // Option + ; 82 | keyCombo = KeyCombo(key: .semicolon, cocoaModifiers: [.option]) 83 | XCTAssertEqual(keyCombo?.characters, "…") 84 | // Shift + F1 85 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: [.shift]) 86 | XCTAssertEqual(keyCombo?.characters, "F1") 87 | // Option double tap 88 | keyCombo = KeyCombo(doubledCocoaModifiers: .option) 89 | XCTAssertEqual(keyCombo?.characters, "") 90 | } 91 | 92 | func testKeyEquivalent() { 93 | var keyCombo: KeyCombo? 94 | // Command + a 95 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.command]) 96 | XCTAssertEqual(keyCombo?.keyEquivalent, "a") 97 | // Shift + a 98 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.shift]) 99 | XCTAssertEqual(keyCombo?.keyEquivalent, "A") 100 | // Option + a 101 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.option]) 102 | XCTAssertEqual(keyCombo?.keyEquivalent, "a") 103 | // Option + Shift + a 104 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.option, .shift]) 105 | XCTAssertEqual(keyCombo?.keyEquivalent, "A") 106 | // Option + Shift + 1 107 | keyCombo = KeyCombo(key: .one, cocoaModifiers: [.option, .shift]) 108 | XCTAssertEqual(keyCombo?.keyEquivalent, "1") 109 | // Option + Shift + Keypad 1 110 | keyCombo = KeyCombo(key: .keypadOne, cocoaModifiers: [.option, .shift]) 111 | XCTAssertEqual(keyCombo?.keyEquivalent, "1") 112 | // Option + ; 113 | keyCombo = KeyCombo(key: .semicolon, cocoaModifiers: [.option]) 114 | XCTAssertEqual(keyCombo?.keyEquivalent, ";") 115 | // Shift + F1 116 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: [.shift]) 117 | XCTAssertEqual(keyCombo?.keyEquivalent, "F1") 118 | // Option double tap 119 | keyCombo = KeyCombo(doubledCocoaModifiers: .option) 120 | XCTAssertEqual(keyCombo?.keyEquivalent, "") 121 | } 122 | 123 | func testKeyEquivalentModifierMaskString() { 124 | var keyCombo: KeyCombo? 125 | // Shift + a 126 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.shift]) 127 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⇧") 128 | // Control + a 129 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.control]) 130 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌃") 131 | // Command + a 132 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.command]) 133 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌘") 134 | // Option + a 135 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.option]) 136 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌥") 137 | // Shift + Control + a 138 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.shift, .control]) 139 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌃⇧") 140 | // Shift + Control + Option + a 141 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.shift, .control, .option]) 142 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌃⌥⇧") 143 | // Command + Option + a 144 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.command, .option]) 145 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌥⌘") 146 | // Command + Shift + Option + Control + a 147 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.command, .shift, .option, .control]) 148 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌃⌥⇧⌘") 149 | // Command + Option + Function + CapsLock + NumericPad + Help + a 150 | keyCombo = KeyCombo(key: .a, cocoaModifiers: [.command, .option, .function, .capsLock, .numericPad, .help]) 151 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌥⌘") 152 | // F1 153 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: []) 154 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "") 155 | // Command + F1 156 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: [.command]) 157 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⌘") 158 | // Shift Double tap 159 | keyCombo = KeyCombo(doubledCocoaModifiers: .shift) 160 | XCTAssertEqual(keyCombo?.keyEquivalentModifierMaskString, "⇧") 161 | } 162 | 163 | func testNSCodingMigrationV3_0() { 164 | var oldKeyCombo: v2_0_0KeyCombo? 165 | var archivedData: Data? 166 | var unarchivedKeyCombo: KeyCombo? 167 | NSKeyedUnarchiver.setClass(KeyCombo.self, forClassName: "MagnetTests.v2_0_0KeyCombo") 168 | // Shift + v 169 | oldKeyCombo = v2_0_0KeyCombo(keyCode: kVK_ANSI_V, modifiers: shiftKey, doubledModifiers: false) 170 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 171 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 172 | XCTAssertNotNil(unarchivedKeyCombo) 173 | XCTAssertEqual(unarchivedKeyCombo?.key, .v) 174 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 175 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 176 | // F3 177 | oldKeyCombo = v2_0_0KeyCombo(keyCode: kVK_F3, modifiers: Int(NSEvent.ModifierFlags.function.rawValue), doubledModifiers: false) 178 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 179 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 180 | XCTAssertNotNil(unarchivedKeyCombo) 181 | XCTAssertEqual(unarchivedKeyCombo?.key, .f3) 182 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, Int(NSEvent.ModifierFlags.function.rawValue)) 183 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 184 | // Control double tap 185 | oldKeyCombo = v2_0_0KeyCombo(keyCode: 0, modifiers: controlKey, doubledModifiers: true) 186 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 187 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 188 | XCTAssertNotNil(unarchivedKeyCombo) 189 | XCTAssertEqual(unarchivedKeyCombo?.key, .a) 190 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, controlKey) 191 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, true) 192 | // Shift + @ 193 | oldKeyCombo = v2_0_0KeyCombo(keyCode: Int(Key.atSign.QWERTYKeyCode), modifiers: shiftKey, doubledModifiers: false) 194 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 195 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 196 | XCTAssertNotNil(unarchivedKeyCombo) 197 | XCTAssertEqual(unarchivedKeyCombo?.key, .leftBracket) 198 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 199 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 200 | } 201 | 202 | func testNSCodingMigrationV3_1() { 203 | var oldKeyCombo: v3_1_0KeyCombo? 204 | var archivedData: Data? 205 | var unarchivedKeyCombo: KeyCombo? 206 | NSKeyedUnarchiver.setClass(KeyCombo.self, forClassName: "MagnetTests.v3_1_0KeyCombo") 207 | // Shift + v 208 | oldKeyCombo = v3_1_0KeyCombo(key: .v, modifiers: shiftKey, doubledModifiers: false) 209 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 210 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 211 | XCTAssertNotNil(unarchivedKeyCombo) 212 | XCTAssertEqual(unarchivedKeyCombo?.key, .v) 213 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 214 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 215 | // F3 216 | oldKeyCombo = v3_1_0KeyCombo(key: .f3, modifiers: Int(NSEvent.ModifierFlags.function.rawValue), doubledModifiers: false) 217 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 218 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 219 | XCTAssertNotNil(unarchivedKeyCombo) 220 | XCTAssertEqual(unarchivedKeyCombo?.key, .f3) 221 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, Int(NSEvent.ModifierFlags.function.rawValue)) 222 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 223 | // Control double tap 224 | oldKeyCombo = v3_1_0KeyCombo(key: .a, modifiers: controlKey, doubledModifiers: true) 225 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 226 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 227 | XCTAssertNotNil(unarchivedKeyCombo) 228 | XCTAssertEqual(unarchivedKeyCombo?.key, .a) 229 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, controlKey) 230 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, true) 231 | // Shift + @ 232 | oldKeyCombo = v3_1_0KeyCombo(key: .atSign, modifiers: shiftKey, doubledModifiers: false) 233 | archivedData = NSKeyedArchiver.archivedData(withRootObject: oldKeyCombo!) 234 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 235 | XCTAssertNotNil(unarchivedKeyCombo) 236 | XCTAssertEqual(unarchivedKeyCombo?.key, .leftBracket) 237 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 238 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 239 | } 240 | 241 | func testNSCoding() { 242 | var keyCombo: KeyCombo? 243 | var archivedData: Data? 244 | var unarchivedKeyCombo: KeyCombo? 245 | // Shift + Control + c 246 | keyCombo = KeyCombo(key: .c, cocoaModifiers: [.shift, .control]) 247 | archivedData = NSKeyedArchiver.archivedData(withRootObject: keyCombo!) 248 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 249 | XCTAssertNotNil(unarchivedKeyCombo) 250 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 251 | // F1 252 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: []) 253 | archivedData = NSKeyedArchiver.archivedData(withRootObject: keyCombo!) 254 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 255 | XCTAssertNotNil(unarchivedKeyCombo) 256 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 257 | // Option double tap 258 | keyCombo = KeyCombo(doubledCocoaModifiers: [.option]) 259 | archivedData = NSKeyedArchiver.archivedData(withRootObject: keyCombo!) 260 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 261 | XCTAssertNotNil(unarchivedKeyCombo) 262 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 263 | // Shift + @ 264 | keyCombo = KeyCombo(key: .atSign, cocoaModifiers: [.shift]) 265 | archivedData = NSKeyedArchiver.archivedData(withRootObject: keyCombo!) 266 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 267 | XCTAssertNotNil(unarchivedKeyCombo) 268 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 269 | // Control + [ 270 | keyCombo = KeyCombo(key: .leftBracket, cocoaModifiers: [.control]) 271 | archivedData = NSKeyedArchiver.archivedData(withRootObject: keyCombo!) 272 | unarchivedKeyCombo = NSKeyedUnarchiver.unarchiveObject(with: archivedData!) as? KeyCombo 273 | XCTAssertNotNil(unarchivedKeyCombo) 274 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 275 | } 276 | 277 | func testCodableMigrationV3_0() throws { 278 | var oldKeyCombo: v2_0_0KeyCombo? 279 | var archivedData: Data? 280 | var unarchivedKeyCombo: KeyCombo? 281 | // Shift + v 282 | oldKeyCombo = v2_0_0KeyCombo(keyCode: kVK_ANSI_V, modifiers: shiftKey, doubledModifiers: false) 283 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 284 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 285 | XCTAssertNotNil(unarchivedKeyCombo) 286 | XCTAssertEqual(unarchivedKeyCombo?.key, .v) 287 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 288 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 289 | // F3 290 | oldKeyCombo = v2_0_0KeyCombo(keyCode: kVK_F3, modifiers: Int(NSEvent.ModifierFlags.function.rawValue), doubledModifiers: false) 291 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 292 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 293 | XCTAssertNotNil(unarchivedKeyCombo) 294 | XCTAssertEqual(unarchivedKeyCombo?.key, .f3) 295 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, Int(NSEvent.ModifierFlags.function.rawValue)) 296 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 297 | // Control double tap 298 | oldKeyCombo = v2_0_0KeyCombo(keyCode: 0, modifiers: controlKey, doubledModifiers: true) 299 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 300 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 301 | XCTAssertNotNil(unarchivedKeyCombo) 302 | XCTAssertEqual(unarchivedKeyCombo?.key, .a) 303 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, controlKey) 304 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, true) 305 | // Shift + @ 306 | oldKeyCombo = v2_0_0KeyCombo(keyCode: Int(Key.atSign.QWERTYKeyCode), modifiers: shiftKey, doubledModifiers: false) 307 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 308 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 309 | XCTAssertNotNil(unarchivedKeyCombo) 310 | XCTAssertEqual(unarchivedKeyCombo?.key, .leftBracket) 311 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 312 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 313 | } 314 | 315 | func testCodableMigrationV3_1() throws { 316 | var oldKeyCombo: v3_1_0KeyCombo? 317 | var archivedData: Data? 318 | var unarchivedKeyCombo: KeyCombo? 319 | // Shift + v 320 | oldKeyCombo = v3_1_0KeyCombo(key: .v, modifiers: shiftKey, doubledModifiers: false) 321 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 322 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 323 | XCTAssertNotNil(unarchivedKeyCombo) 324 | XCTAssertEqual(unarchivedKeyCombo?.key, .v) 325 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 326 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 327 | // F3 328 | oldKeyCombo = v3_1_0KeyCombo(key: .f3, modifiers: Int(NSEvent.ModifierFlags.function.rawValue), doubledModifiers: false) 329 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 330 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 331 | XCTAssertNotNil(unarchivedKeyCombo) 332 | XCTAssertEqual(unarchivedKeyCombo?.key, .f3) 333 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, Int(NSEvent.ModifierFlags.function.rawValue)) 334 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 335 | // Control double tap 336 | oldKeyCombo = v3_1_0KeyCombo(key: .a, modifiers: controlKey, doubledModifiers: true) 337 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 338 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 339 | XCTAssertNotNil(unarchivedKeyCombo) 340 | XCTAssertEqual(unarchivedKeyCombo?.key, .a) 341 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, controlKey) 342 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, true) 343 | // Shift + @ 344 | oldKeyCombo = v3_1_0KeyCombo(key: .atSign, modifiers: shiftKey, doubledModifiers: false) 345 | archivedData = try JSONEncoder().encode(oldKeyCombo!) 346 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 347 | XCTAssertNotNil(unarchivedKeyCombo) 348 | XCTAssertEqual(unarchivedKeyCombo?.key, .leftBracket) 349 | XCTAssertEqual(unarchivedKeyCombo?.modifiers, shiftKey) 350 | XCTAssertEqual(unarchivedKeyCombo?.doubledModifiers, false) 351 | } 352 | 353 | func testCodable() throws { 354 | var keyCombo: KeyCombo? 355 | var archivedData: Data? 356 | var unarchivedKeyCombo: KeyCombo? 357 | // Shift + Control + c 358 | keyCombo = KeyCombo(key: .c, cocoaModifiers: [.shift, .control]) 359 | archivedData = try JSONEncoder().encode(keyCombo!) 360 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 361 | XCTAssertNotNil(unarchivedKeyCombo) 362 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 363 | // F1 364 | keyCombo = KeyCombo(key: .f1, cocoaModifiers: []) 365 | archivedData = try JSONEncoder().encode(keyCombo!) 366 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 367 | XCTAssertNotNil(unarchivedKeyCombo) 368 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 369 | // Option double tap 370 | keyCombo = KeyCombo(doubledCocoaModifiers: [.option]) 371 | archivedData = try JSONEncoder().encode(keyCombo!) 372 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 373 | XCTAssertNotNil(unarchivedKeyCombo) 374 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 375 | // Shift + @ 376 | keyCombo = KeyCombo(key: .atSign, cocoaModifiers: [.shift]) 377 | archivedData = try JSONEncoder().encode(keyCombo!) 378 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 379 | XCTAssertNotNil(unarchivedKeyCombo) 380 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 381 | // Control + [ 382 | keyCombo = KeyCombo(key: .leftBracket, cocoaModifiers: [.control]) 383 | archivedData = try JSONEncoder().encode(keyCombo!) 384 | unarchivedKeyCombo = try JSONDecoder().decode(KeyCombo.self, from: archivedData!) 385 | XCTAssertNotNil(unarchivedKeyCombo) 386 | XCTAssertEqual(keyCombo, unarchivedKeyCombo) 387 | } 388 | 389 | } 390 | -------------------------------------------------------------------------------- /Lib/MagnetTests/ModifierEventHandlerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModifierEventHandlerTests.swift 3 | // 4 | // MagnetTests 5 | // GitHub: https://github.com/clipy 6 | // HP: https://clipy-app.com 7 | // 8 | // Copyright © 2015-2020 Clipy Project. 9 | // 10 | 11 | import XCTest 12 | @testable import Magnet 13 | 14 | // swiftlint:disable xctfail_message 15 | final class ModifierEventHandlerTests: XCTestCase { 16 | 17 | private let testTimeInterval = DispatchTimeInterval.milliseconds(100) 18 | private let testQueue = DispatchQueue(label: "com.clipy-app.Magnet") 19 | 20 | // MARK: - Tests 21 | func testDoubleTapModifierEvent() { 22 | let eventHandler = ModifierEventHandler(cleanQueue: testQueue) 23 | let expectation = XCTestExpectation(description: "Shift double tapped") 24 | eventHandler.doubleTapped = { tappedModifierFlags in 25 | XCTAssertEqual(tappedModifierFlags, [.shift]) 26 | expectation.fulfill() 27 | } 28 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 0) 29 | eventHandler.handleModifiersEvent(with: [], timestamp: 1) 30 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 2) 31 | wait(for: [expectation], timeout: 1) 32 | } 33 | 34 | func testFilterHandledTimestampModifierEvent() { 35 | let eventHandler = ModifierEventHandler(cleanQueue: testQueue) 36 | eventHandler.doubleTapped = { tappedModifierFlags in 37 | XCTFail() 38 | } 39 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 0) 40 | eventHandler.handleModifiersEvent(with: [], timestamp: 0) 41 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 0) 42 | } 43 | 44 | func testFilerUnsupportModifierEvent() { 45 | let eventHandler = ModifierEventHandler(cleanQueue: testQueue) 46 | eventHandler.doubleTapped = { tappedModifierFlags in 47 | XCTFail() 48 | } 49 | eventHandler.handleModifiersEvent(with: [.function], timestamp: 0) 50 | eventHandler.handleModifiersEvent(with: [], timestamp: 1) 51 | eventHandler.handleModifiersEvent(with: [.function], timestamp: 2) 52 | } 53 | 54 | func testMultiModifierEvent() { 55 | let eventHandler = ModifierEventHandler(cleanQueue: testQueue) 56 | let expectation = XCTestExpectation(description: "Shift double tapped") 57 | var isFirstCalled = false 58 | eventHandler.doubleTapped = { tappedModifierFlags in 59 | if isFirstCalled { 60 | XCTFail() 61 | } else { 62 | XCTAssertEqual(tappedModifierFlags, [.shift]) 63 | isFirstCalled = true 64 | } 65 | expectation.fulfill() 66 | } 67 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 0) 68 | eventHandler.handleModifiersEvent(with: [], timestamp: 1) 69 | eventHandler.handleModifiersEvent(with: [.control], timestamp: 2) 70 | eventHandler.handleModifiersEvent(with: [], timestamp: 3) 71 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 4) 72 | eventHandler.handleModifiersEvent(with: [], timestamp: 5) 73 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 6) 74 | wait(for: [expectation], timeout: 1) 75 | } 76 | 77 | func testSimultaneouslyPressMultiModifierEvent() { 78 | let eventHandler = ModifierEventHandler(cleanQueue: testQueue) 79 | let expectation = XCTestExpectation(description: "Shift double tapped") 80 | var isFirstCalled = false 81 | eventHandler.doubleTapped = { tappedModifierFlags in 82 | if isFirstCalled { 83 | XCTFail() 84 | } else { 85 | XCTAssertEqual(tappedModifierFlags, [.shift]) 86 | isFirstCalled = true 87 | } 88 | expectation.fulfill() 89 | } 90 | eventHandler.handleModifiersEvent(with: [.shift, .control], timestamp: 0) 91 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 1) 92 | eventHandler.handleModifiersEvent(with: [], timestamp: 2) 93 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 3) 94 | eventHandler.handleModifiersEvent(with: [], timestamp: 4) 95 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 5) 96 | wait(for: [expectation], timeout: 1) 97 | } 98 | 99 | func testNoCleanModifierEvent() { 100 | let eventHandler = ModifierEventHandler(cleanTimeInterval: testTimeInterval, cleanQueue: testQueue) 101 | let expectation = XCTestExpectation(description: "Shift double tapped") 102 | eventHandler.doubleTapped = { tappedModifierFlags in 103 | XCTAssertEqual(tappedModifierFlags, [.shift]) 104 | expectation.fulfill() 105 | } 106 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 0) 107 | eventHandler.handleModifiersEvent(with: [], timestamp: 1) 108 | testQueue.asyncAfter(deadline: .now() + testTimeInterval - .milliseconds(1)) { 109 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 2) 110 | } 111 | wait(for: [expectation], timeout: 1) 112 | } 113 | 114 | func testCleanModifierEvent() { 115 | let eventHandler = ModifierEventHandler(cleanTimeInterval: testTimeInterval, cleanQueue: testQueue) 116 | let expectation = XCTestExpectation(description: "Shift double tapped") 117 | eventHandler.doubleTapped = { tappedModifierFlags in 118 | XCTFail() 119 | } 120 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 0) 121 | eventHandler.handleModifiersEvent(with: [], timestamp: 1) 122 | testQueue.asyncAfter(deadline: .now() + testTimeInterval + .milliseconds(1)) { 123 | eventHandler.handleModifiersEvent(with: [.shift], timestamp: 2) 124 | expectation.fulfill() 125 | } 126 | wait(for: [expectation], timeout: 1) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Magnet.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Magnet" 3 | s.version = "3.4.0" 4 | s.summary = "Customize global hotkeys in macOS." 5 | s.license = { :type => 'MIT', :file => 'LICENSE' } 6 | s.homepage = "https://github.com/Clipy/Magnet" 7 | s.author = { "Econa77" => "f.s.1992.ip@gmail.com" } 8 | s.source = { :git => "https://github.com/Clipy/Magnet.git", :tag => "v#{s.version}" } 9 | s.platform = :osx, '10.13' 10 | s.source_files = 'Lib/Magnet/**/*.swift' 11 | s.swift_version = '5.0' 12 | s.frameworks = 'Carbon', 'Cocoa' 13 | s.dependency 'Sauce', '~> 2.4.0' 14 | end 15 | -------------------------------------------------------------------------------- /Magnet.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Magnet.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Magnet", 7 | platforms: [ 8 | .macOS(.v10_13) 9 | ], 10 | products: [ 11 | .library( 12 | name: "Magnet", 13 | targets: ["Magnet"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/Clipy/Sauce", .upToNextMinor(from: "2.4.0")), 17 | ], 18 | targets: [ 19 | .target( 20 | name: "Magnet", 21 | dependencies: ["Sauce"], 22 | path: "Lib/Magnet"), 23 | .testTarget( 24 | name: "MagnetTests", 25 | dependencies: ["Magnet"], 26 | path: "Lib/MagnetTests"), 27 | ], 28 | swiftLanguageVersions: [.v5] 29 | ) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Magnet 2 | ![CI](https://github.com/Clipy/Magnet/workflows/Xcode-Build/badge.svg) 3 | [![Release version](https://img.shields.io/github/release/Clipy/Magnet.svg)](https://github.com/Clipy/Magnet/releases/latest) 4 | [![License: MIT](https://img.shields.io/github/license/Clipy/Magnet.svg)](https://github.com/Clipy/Magnet/blob/master/LICENSE) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | [![Version](https://img.shields.io/cocoapods/v/Magnet.svg)](http://cocoadocs.org/docsets/Magnet) 7 | [![Platform](https://img.shields.io/cocoapods/p/Magnet.svg)](http://cocoadocs.org/docsets/Magnet) 8 | [![SPM supported](https://img.shields.io/badge/SPM-supported-DE5C43.svg?style=flat)](https://swift.org/package-manager) 9 | 10 | Customize global hotkeys in macOS. Supports usual hotkey and double tap hotkey like Alfred.app. 11 | 12 | Also supports sandbox application. 13 | 14 | ## Usage 15 | ### CocoaPods 16 | ``` 17 | pod 'Magnet' 18 | ``` 19 | 20 | ### Carthage 21 | ``` 22 | github "Clipy/Magnet" 23 | ``` 24 | 25 | ## Upgrading from Magnet v2.x to v3.x 26 | See [Upgrading from Magnet v2.x](/Documentation/Upgrading_Magnet_2.md) 27 | 28 | ## Example 29 | ### Register Normal hotkey 30 | Add `⌘ + Control + B` hotkey. 31 | 32 | ```swift 33 | if let keyCombo = KeyCombo(key: .b, cocoaModifiers: [.command, .control]]) { 34 | let hotKey = HotKey(identifier: "CommandControlB", keyCombo: keyCombo, target: self, action: #selector()) 35 | hotKey.register() // or HotKeyCenter.shared.register(with: hotKey) 36 | } 37 | ``` 38 | 39 | Or you can use closures. 40 | ```swift 41 | if let keyCombo = KeyCombo(key: .b, cocoaModifiers: [.command, .control]) { 42 | let hotKey = HotKey(identifier: "CommandControlB", keyCombo: keyCombo) { hotKey in 43 | // Called when ⌘ + Control + B is pressed 44 | } 45 | hotKey.register() 46 | } 47 | ``` 48 | 49 | ### Register Double tap hotkey 50 | Add `⌘ double tap` hotkey. 51 | ```swift 52 | if let keyCombo = KeyCombo(doubledCocoaModifiers: .command) { 53 | let hotKey = HotKey(identifier: "CommandDoubleTap", keyCombo: keyCombo, target: self, action: #selector()) 54 | hotKey.register() // or HotKeyCenter.shared.register(with: hotKey) 55 | } 56 | ``` 57 | 58 | Add `Control double tap` hotkey. 59 | ```swift 60 | if let keyCombo = KeyCombo(doubledCarbonModifiers: controlKey) { 61 | let hotKey = HotKey(identifier: "ControlDoubleTap", keyCombo: keyCombo, target: self, action: #selector()) 62 | hotKey.register() // or HotKeyCenter.shared.register(with: hotKey) 63 | } 64 | ``` 65 | 66 | #### Support modifiers 67 | Double tap hotkey only support following modifiers. 68 | - Command Key 69 | - `NSEventModifierFlags.command` or `cmdKey` 70 | - Shift Key 71 | - `NSEventModifierFlags.shift` or `shiftKey` 72 | - Option Key 73 | - `NSEventModifierFlags.option` or `optionKey` 74 | - Control Key 75 | - `NSEventModifierFlags.control` or `controlKey` 76 | 77 | ### Unregister hotkeys 78 | ```swift 79 | HotKeyCenter.shared.unregisterAll() 80 | ``` 81 | 82 | or 83 | 84 | ```swift 85 | HotKeyCenter.shared.unregisterHotKey(with: "identifier") 86 | ``` 87 | 88 | or 89 | 90 | ```swift 91 | let hotKey = HotKey(identifier: "identifier", keyCombo: KeyCombo, target: self, action: #selector()) 92 | hotKey.unregister() // or HotKeyCenter.shared.unregister(with: hotKey) 93 | ``` 94 | 95 | ## Dependencies 96 | - [Sauce](https://github.com/Clipy/Sauce) 97 | 98 | ## How to Build 99 | 1. Move to the project root directory 100 | 2. Install dependency library with `carthage` or `git submodule` 101 | 3. `carthage checkout --use-submodules` or `git submodule update --init --recursive` 102 | 4. Open `Magnet.xcworkspace` on Xcode. 103 | 5. build. 104 | --------------------------------------------------------------------------------