diff --git a/English.lproj/MainMenu.xib b/English.lproj/MainMenu.xib index acc66ff..5378e5b 100644 --- a/English.lproj/MainMenu.xib +++ b/English.lproj/MainMenu.xib @@ -768,9 +768,6 @@ ApplicationController - - GlobalHotkeys - RemoteHostsManager @@ -1768,11 +1765,6 @@ - - 586 - - - 589 diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index 3b1ff49..9473ed0 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -20,11 +20,10 @@ 3511697810D68B1000A5FAA1 /* FilesCountTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3511697710D68B1000A5FAA1 /* FilesCountTransformer.m */; }; 35116A7010D6AAD200A5FAA1 /* Menulet.m in Sources */ = {isa = PBXBuildFile; fileRef = 35116A6F10D6AAD200A5FAA1 /* Menulet.m */; }; 35116AC010D6AF0500A5FAA1 /* LoginItem.m in Sources */ = {isa = PBXBuildFile; fileRef = 35116ABF10D6AF0500A5FAA1 /* LoginItem.m */; }; - 35116ADA10D6B00200A5FAA1 /* Hotkey.m in Sources */ = {isa = PBXBuildFile; fileRef = 35116AD910D6B00200A5FAA1 /* Hotkey.m */; }; 3513A600113908A900AD789D /* Read Only.png in Resources */ = {isa = PBXBuildFile; fileRef = 3513A5FF113908A900AD789D /* Read Only.png */; }; 3513A61C11390B7900AD789D /* Error.m in Sources */ = {isa = PBXBuildFile; fileRef = 3513A61B11390B7900AD789D /* Error.m */; }; 351416CF28C3A7B80093A452 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 351416CE28C3A7B80093A452 /* Sparkle */; }; - AA1B2C3D4E5F000100000001 /* ShortcutRecorder in Frameworks */ = {isa = PBXBuildFile; productRef = AA1B2C3D4E5F000200000001 /* ShortcutRecorder */; }; + AA1B2C3D4E5F000100000001 /* MASShortcut in Frameworks */ = {isa = PBXBuildFile; productRef = AA1B2C3D4E5F000200000001 /* MASShortcut */; }; 3516864B11187EDE00314609 /* HostsTextController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3516864A11187EDE00314609 /* HostsTextController.m */; }; 351D904510E76F7100CA6B5E /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 351D904110E76F7100CA6B5E /* Help */; }; 3522BEE0153316AA00035B90 /* ExtendedNSArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 3522BEDF153316AA00035B90 /* ExtendedNSArray.m */; }; @@ -103,7 +102,6 @@ 35B0499B1A462AE900EB89CA /* Activated@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B0499A1A462AE900EB89CA /* Activated@2x.png */; }; 35B0499D1A462BC500EB89CA /* Blue Dot@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B0499C1A462BC500EB89CA /* Blue Dot@2x.png */; }; 35B36A641263B25A005F6A66 /* DebugUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 35B36A631263B25A005F6A66 /* DebugUtil.m */; }; - 35C8D58611144E9C00B4242D /* GlobalHotkeys.m in Sources */ = {isa = PBXBuildFile; fileRef = 35C8D58511144E9C00B4242D /* GlobalHotkeys.m */; }; 35C8D5A911144F7000B4242D /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35C8D5A811144F7000B4242D /* Carbon.framework */; }; BB1A2B3C4D5E000200000001 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB1A2B3C4D5E000100000001 /* ServiceManagement.framework */; }; 35D2C253113507A6007C8037 /* RemoteHostsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D2C252113507A6007C8037 /* RemoteHostsManager.m */; }; @@ -140,6 +138,7 @@ AA000010000000000000AAAA /* URLSheetViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000F000000000000AAAA /* URLSheetViewTests.swift */; }; BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000001000000000000BBBB /* RemoteIntervalMapper.swift */; }; BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000003000000000000BBBB /* ShortcutRecorderView.swift */; }; + CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000CC01 /* GlobalShortcuts.swift */; }; BB000006000000000000BBBB /* SparkleObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000005000000000000BBBB /* SparkleObserver.swift */; }; BB000008000000000000BBBB /* LoginItemObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000007000000000000BBBB /* LoginItemObserver.swift */; }; BB00000A000000000000BBBB /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000009000000000000BBBB /* PreferencesView.swift */; }; @@ -147,6 +146,8 @@ BB00000E000000000000BBBB /* RemoteIntervalMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */; }; BB000010000000000000BBBB /* SparkleObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB00000F000000000000BBBB /* SparkleObserverTests.swift */; }; BB000012000000000000BBBB /* PreferencesPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000011000000000000BBBB /* PreferencesPresenterTests.swift */; }; + DD000002000000000000DD01 /* GlobalShortcutsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000001000000000000DD01 /* GlobalShortcutsTests.swift */; }; + DD000004000000000000DD01 /* ShortcutRecorderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */; }; /* Begin PBXContainerItemProxy section */ 353D18A01114C067005C4E54 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -207,8 +208,6 @@ 35116A6F10D6AAD200A5FAA1 /* Menulet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Menulet.m; path = Source/Menulet.m; sourceTree = ""; }; 35116ABE10D6AF0500A5FAA1 /* LoginItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LoginItem.h; path = Source/LoginItem.h; sourceTree = ""; }; 35116ABF10D6AF0500A5FAA1 /* LoginItem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LoginItem.m; path = Source/LoginItem.m; sourceTree = ""; }; - 35116AD810D6B00200A5FAA1 /* Hotkey.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Hotkey.h; path = Source/Hotkey.h; sourceTree = ""; }; - 35116AD910D6B00200A5FAA1 /* Hotkey.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Hotkey.m; path = Source/Hotkey.m; sourceTree = ""; }; 3513A5FF113908A900AD789D /* Read Only.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Read Only.png"; path = "Resources/Images/Read Only.png"; sourceTree = ""; }; 3513A61A11390B7900AD789D /* Error.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Error.h; path = Source/Error.h; sourceTree = ""; }; 3513A61B11390B7900AD789D /* Error.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Error.m; path = Source/Error.m; sourceTree = ""; }; @@ -332,6 +331,7 @@ AA00000F000000000000AAAA /* URLSheetViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetViewTests.swift; sourceTree = ""; }; BB000001000000000000BBBB /* RemoteIntervalMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RemoteIntervalMapper.swift; path = "Source/Swift/RemoteIntervalMapper.swift"; sourceTree = ""; }; BB000003000000000000BBBB /* ShortcutRecorderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShortcutRecorderView.swift; path = "Source/Swift/ShortcutRecorderView.swift"; sourceTree = ""; }; + CC000001000000000000CC01 /* GlobalShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GlobalShortcuts.swift; path = "Source/Swift/GlobalShortcuts.swift"; sourceTree = ""; }; BB000005000000000000BBBB /* SparkleObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SparkleObserver.swift; path = "Source/Swift/SparkleObserver.swift"; sourceTree = ""; }; BB000007000000000000BBBB /* LoginItemObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginItemObserver.swift; path = "Source/Swift/LoginItemObserver.swift"; sourceTree = ""; }; BB000009000000000000BBBB /* PreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesView.swift; path = "Source/Swift/PreferencesView.swift"; sourceTree = ""; }; @@ -339,13 +339,13 @@ BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteIntervalMapperTests.swift; sourceTree = ""; }; BB00000F000000000000BBBB /* SparkleObserverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SparkleObserverTests.swift; sourceTree = ""; }; BB000011000000000000BBBB /* PreferencesPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPresenterTests.swift; sourceTree = ""; }; + DD000001000000000000DD01 /* GlobalShortcutsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalShortcutsTests.swift; sourceTree = ""; }; + DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutRecorderViewTests.swift; sourceTree = ""; }; 35A4CD2F1534927F005176BD /* Combined Hosts Hint.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Combined Hosts Hint.png"; path = "Resources/Images/Combined Hosts Hint.png"; sourceTree = ""; }; 35B0499A1A462AE900EB89CA /* Activated@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Activated@2x.png"; path = "Resources/Images/Activated@2x.png"; sourceTree = ""; }; 35B0499C1A462BC500EB89CA /* Blue Dot@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Blue Dot@2x.png"; path = "Resources/Images/Blue Dot@2x.png"; sourceTree = ""; }; 35B36A621263B25A005F6A66 /* DebugUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DebugUtil.h; path = Source/DebugUtil.h; sourceTree = ""; }; 35B36A631263B25A005F6A66 /* DebugUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DebugUtil.m; path = Source/DebugUtil.m; sourceTree = ""; }; - 35C8D58411144E9C00B4242D /* GlobalHotkeys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GlobalHotkeys.h; path = Source/GlobalHotkeys.h; sourceTree = ""; }; - 35C8D58511144E9C00B4242D /* GlobalHotkeys.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GlobalHotkeys.m; path = Source/GlobalHotkeys.m; sourceTree = ""; }; 35C8D5A811144F7000B4242D /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; BB1A2B3C4D5E000100000001 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 35D2C251113507A6007C8037 /* RemoteHostsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RemoteHostsManager.h; path = Source/RemoteHostsManager.h; sourceTree = ""; }; @@ -399,7 +399,7 @@ buildActionMask = 2147483647; files = ( 351416CF28C3A7B80093A452 /* Sparkle in Frameworks */, - AA1B2C3D4E5F000100000001 /* ShortcutRecorder in Frameworks */, + AA1B2C3D4E5F000100000001 /* MASShortcut in Frameworks */, 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */, 354E7F8110AEFBC500FC4757 /* Security.framework in Frameworks */, 3545DCE410E3997200EBA66D /* SystemConfiguration.framework in Frameworks */, @@ -472,7 +472,6 @@ 35D4C98210E64D8800B9F63A /* Extended Next Step */, 3562196F10E52E5D00772473 /* About Box */, 35116A9610D6AD2C00A5FAA1 /* Preferences */, - 35C8D58011144E8300B4242D /* Hotkeys */, 355F5C4C12244AF5006C2884 /* Local Hosts */, 35D3328B152F5A45001DA824 /* Combined Hosts */, 35730FCA113424A500066B68 /* Remote Hosts */, @@ -763,6 +762,7 @@ isa = PBXGroup; children = ( AA000002000000000000AAAA /* GasMask-Bridging-Header.h */, + CC000001000000000000CC01 /* GlobalShortcuts.swift */, AA000003000000000000AAAA /* URLValidator.swift */, AA000005000000000000AAAA /* NetworkStatusObserver.swift */, AA000007000000000000AAAA /* URLSheetView.swift */, @@ -785,17 +785,6 @@ name = Preferences; sourceTree = ""; }; - 35C8D58011144E8300B4242D /* Hotkeys */ = { - isa = PBXGroup; - children = ( - 35116AD810D6B00200A5FAA1 /* Hotkey.h */, - 35116AD910D6B00200A5FAA1 /* Hotkey.m */, - 35C8D58411144E9C00B4242D /* GlobalHotkeys.h */, - 35C8D58511144E9C00B4242D /* GlobalHotkeys.m */, - ); - name = Hotkeys; - sourceTree = ""; - }; 35D3328B152F5A45001DA824 /* Combined Hosts */ = { isa = PBXGroup; children = ( @@ -864,6 +853,8 @@ BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */, BB00000F000000000000BBBB /* SparkleObserverTests.swift */, BB000011000000000000BBBB /* PreferencesPresenterTests.swift */, + DD000001000000000000DD01 /* GlobalShortcutsTests.swift */, + DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */, CC2B3C4D5E6F000100000010 /* NodeTests.m */, CC2B3C4D5E6F000100000011 /* HostsTests.m */, CC2B3C4D5E6F000100000012 /* HostsGroupTests.m */, @@ -920,7 +911,7 @@ name = "Gas Mask"; packageProductDependencies = ( 351416CE28C3A7B80093A452 /* Sparkle */, - AA1B2C3D4E5F000200000001 /* ShortcutRecorder */, + AA1B2C3D4E5F000200000001 /* MASShortcut */, ); productInstallPath = "$(HOME)/Applications"; productName = "Gas Mask"; @@ -967,7 +958,7 @@ mainGroup = 29B97314FDCFA39411CA2CEA /* Gas Mask */; packageReferences = ( 351416CD28C3A7B80093A452 /* XCRemoteSwiftPackageReference "Sparkle" */, - AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "ShortcutRecorder" */, + AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "MASShortcut" */, ); productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; @@ -1080,7 +1071,6 @@ 3511697810D68B1000A5FAA1 /* FilesCountTransformer.m in Sources */, 35116A7010D6AAD200A5FAA1 /* Menulet.m in Sources */, 35116AC010D6AF0500A5FAA1 /* LoginItem.m in Sources */, - 35116ADA10D6B00200A5FAA1 /* Hotkey.m in Sources */, 358448B310DFE283002E6A9B /* Util.m in Sources */, 3545DCA910E396CF00EBA66D /* Network.m in Sources */, 3541CE0510E4AF9B00FA00CB /* SyncingArrowsBadge.m in Sources */, @@ -1090,7 +1080,6 @@ 35D4CBFB10E6934700B9F63A /* ExtendedNSTextView.m in Sources */, 35738409110209AB00E8DB54 /* HostsListView.m in Sources */, 3597135D110DED0F00C7ECAF /* HostsMenu.m in Sources */, - 35C8D58611144E9C00B4242D /* GlobalHotkeys.m in Sources */, 354CCF811117285300EB6948 /* HostsListViewMenu.m in Sources */, 3516864B11187EDE00314609 /* HostsTextController.m in Sources */, 356712201132E2A700DF30E0 /* NetworkStatus.m in Sources */, @@ -1103,6 +1092,7 @@ AA00000A000000000000AAAA /* URLSheetPresenter.swift in Sources */, BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */, BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */, + CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */, BB000006000000000000BBBB /* SparkleObserver.swift in Sources */, BB000008000000000000BBBB /* LoginItemObserver.swift in Sources */, BB00000A000000000000BBBB /* PreferencesView.swift in Sources */, @@ -1151,6 +1141,8 @@ BB00000E000000000000BBBB /* RemoteIntervalMapperTests.swift in Sources */, BB000010000000000000BBBB /* SparkleObserverTests.swift in Sources */, BB000012000000000000BBBB /* PreferencesPresenterTests.swift in Sources */, + DD000002000000000000DD01 /* GlobalShortcutsTests.swift in Sources */, + DD000004000000000000DD01 /* ShortcutRecorderViewTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1492,12 +1484,12 @@ version = 1.27.1; }; }; - AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "ShortcutRecorder" */ = { + AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "MASShortcut" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/Kentzo/ShortcutRecorder.git"; + repositoryURL = "https://github.com/shpakovski/MASShortcut"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 3.4.0; + kind = revision; + revision = 6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -1508,10 +1500,10 @@ package = 351416CD28C3A7B80093A452 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; - AA1B2C3D4E5F000200000001 /* ShortcutRecorder */ = { + AA1B2C3D4E5F000200000001 /* MASShortcut */ = { isa = XCSwiftPackageProductDependency; - package = AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "ShortcutRecorder" */; - productName = ShortcutRecorder; + package = AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "MASShortcut" */; + productName = MASShortcut; }; /* End XCSwiftPackageProductDependency section */ }; diff --git a/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index bbd82e2..891922a 100644 --- a/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -2,12 +2,12 @@ "originHash" : "74784ff33407b9ca71d04ea3acb135980046aa512ad6ef909abb7da8127b1333", "pins" : [ { - "identity" : "shortcutrecorder", + "identity" : "masshortcut", "kind" : "remoteSourceControl", - "location" : "https://github.com/Kentzo/ShortcutRecorder.git", + "location" : "https://github.com/shpakovski/MASShortcut", "state" : { - "revision" : "c86ce0f9be5353ba998966121c7631602a9a36f7", - "version" : "3.4.0" + "branch" : "master", + "revision" : "6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a" } }, { diff --git a/Source/ApplicationController.m b/Source/ApplicationController.m index 501305e..b35f19d 100644 --- a/Source/ApplicationController.m +++ b/Source/ApplicationController.m @@ -177,8 +177,10 @@ - (void)decreaseBusyThreadsCount:(NSNotification *)notification - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { + (void)[GlobalShortcuts shared]; // Register global hotkeys + [NSApp setServicesProvider:self]; - + [self initStructure]; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; diff --git a/Source/GlobalHotkeys.h b/Source/GlobalHotkeys.h deleted file mode 100644 index a475d22..0000000 --- a/Source/GlobalHotkeys.h +++ /dev/null @@ -1,27 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2010 by Clockwise * - * copyright@clockwise.ee * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * - ***************************************************************************/ - - -@interface GlobalHotkeys : NSObject { - @private - NSMutableDictionary *ids; -} - -@end diff --git a/Source/GlobalHotkeys.m b/Source/GlobalHotkeys.m deleted file mode 100644 index 85ba920..0000000 --- a/Source/GlobalHotkeys.m +++ /dev/null @@ -1,145 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2010 by Clockwise * - * copyright@clockwise.ee * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * - ***************************************************************************/ - -#import - -#import "GlobalHotkeys.h" -#import "Hotkey.h" -#import "Preferences.h" - -#define ActivatePreviousFileHotkeyID 1 -#define ActivateNextFileHotkeyID 2 -#define UpdateAndSynchronizeHotkeyID 3 - -@interface GlobalHotkeys () -- (void)registerHotkeyWithID:(int)hotkeyID preferenceKey:(NSString*)key; -- (void)unregisterHotkey:(EventHotKeyRef)hotkeyRef; -@end - - -@implementation GlobalHotkeys - -OSStatus HotkeyHandler(EventHandlerCallRef nextHandler,EventRef theEvent, - void *userData) -{ - EventHotKeyID hkCom; - GetEventParameter(theEvent, kEventParamDirectObject, typeEventHotKeyID, NULL, - sizeof(hkCom), NULL, &hkCom); - int l = hkCom.id; - - NSString *notificationName; - - switch (l) { - case ActivatePreviousFileHotkeyID: - notificationName = ActivatePreviousFileNotification; - break; - case ActivateNextFileHotkeyID: - notificationName = ActivateNextFileNotification; - break; - case UpdateAndSynchronizeHotkeyID: - notificationName = UpdateAndSynchronizeNotification; - break; - default: - return noErr; - } - - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - [nc postNotificationName:notificationName object:nil]; - - return noErr; -} - -- (id)init -{ - self = [super init]; - ids = [[NSMutableDictionary alloc] init]; - - EventTypeSpec eventType; - eventType.eventClass = kEventClassKeyboard; - eventType.eventKind = kEventHotKeyPressed; - - InstallApplicationEventHandler(&HotkeyHandler, 1, &eventType, NULL, NULL); - - [self registerHotkeyWithID:ActivatePreviousFileHotkeyID - preferenceKey:ActivatePreviousFilePrefKey]; - - [self registerHotkeyWithID:ActivateNextFileHotkeyID - preferenceKey:ActivateNextFilePrefKey]; - - [self registerHotkeyWithID:UpdateAndSynchronizeHotkeyID - preferenceKey:UpdateAndSynchronizePrefKey]; - - return self; -} - -- (void)registerHotkeyWithID:(int)hotkeyID preferenceKey:(NSString*)key -{ - NSUserDefaults *defaults = [[Preferences instance] defaults]; - - id plist = [defaults valueForKey:key]; - Hotkey *hotkey = [[Hotkey alloc] initWithPlistRepresentation:plist]; - - EventHotKeyID eventHotKeyID; - eventHotKeyID.id = hotkeyID; - EventHotKeyRef hotkeyRef; - - RegisterEventHotKey([hotkey keyCode], - [hotkey modifiers], - eventHotKeyID, - GetApplicationEventTarget(), - 0, - &hotkeyRef); - if ([ids objectForKey:key] != nil) { - [ids removeObjectForKey:key]; - } - [ids setObject:[NSNumber numberWithInt:hotkeyID] forKey:key]; - - [defaults addObserver:self - forKeyPath:key - options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew - context:hotkeyRef]; -} - -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - id old = [change objectForKey:@"old"]; - if (old != nil && ![old isKindOfClass:[NSNull class]] ) { //TODO: check nil or NSNull - int oldKeyCode = [[old objectForKey:@"keyCode"] intValue]; - int oldModifiers = [[old objectForKey:@"modifiers"] intValue]; - - id new = [change objectForKey:@"new"]; - int keyCode = [[new objectForKey:@"keyCode"] intValue]; - int modifiers = [[new objectForKey:@"modifiers"] intValue]; - - if (oldKeyCode != keyCode || oldModifiers != modifiers) { - - [self unregisterHotkey:context]; - [self registerHotkeyWithID:[[ids objectForKey:keyPath] intValue] preferenceKey:keyPath]; - } - } - -} - -- (void)unregisterHotkey:(EventHotKeyRef)hotkeyRef -{ - UnregisterEventHotKey(hotkeyRef); -} - -@end diff --git a/Source/Hotkey.h b/Source/Hotkey.h deleted file mode 100644 index 40655f0..0000000 --- a/Source/Hotkey.h +++ /dev/null @@ -1,38 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2010 by Clockwise * - * copyright@clockwise.ee * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * - ***************************************************************************/ - -#import - - -@interface Hotkey : NSObject { - @private - int keyCode; - int modifiers; -} - -- (id)initWithKeyCode: (int)mKeyCode modifiers: (int)mModifiers; -- (id)initWithPlistRepresentation: (id)plist; - -- (id)plistRepresentation; - -@property int keyCode; -@property int modifiers; - -@end diff --git a/Source/Hotkey.m b/Source/Hotkey.m deleted file mode 100644 index 8df43bc..0000000 --- a/Source/Hotkey.m +++ /dev/null @@ -1,67 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2010 by Clockwise * - * copyright@clockwise.ee * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * - ***************************************************************************/ - -#import "Hotkey.h" - -@implementation Hotkey - -@synthesize keyCode; -@synthesize modifiers; - -- (id)initWithKeyCode: (int)mKeyCode modifiers: (int)mModifiers; -{ - self = [super init]; - keyCode = mKeyCode; - modifiers = mModifiers; - return self; -} - -- (id)initWithPlistRepresentation: (id)plist -{ - self = [super init]; - - if(!plist || ![plist count]) { - keyCode = -1; - modifiers = -1; - } - else { - keyCode = [[plist objectForKey: @"keyCode"] intValue]; - if(keyCode <= 0) { - keyCode = -1; - } - - modifiers = [[plist objectForKey: @"modifiers"] intValue]; - if(modifiers <= 0) { - modifiers = -1; - } - } - - return self; -} - -- (id)plistRepresentation -{ - return [NSDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithInt: keyCode], @"keyCode", - [NSNumber numberWithInt: modifiers], @"modifiers", - nil]; -} - -@end diff --git a/Source/Swift/GasMask-Bridging-Header.h b/Source/Swift/GasMask-Bridging-Header.h index d6b3ce2..f9df72d 100644 --- a/Source/Swift/GasMask-Bridging-Header.h +++ b/Source/Swift/GasMask-Bridging-Header.h @@ -2,7 +2,6 @@ #import "Network.h" #import "HostsMainController.h" #import "RemoteHostsController.h" -#import "Hotkey.h" #import "Preferences.h" #import "Preferences+Remote.h" #import "LoginItem.h" diff --git a/Source/Swift/GlobalShortcuts.swift b/Source/Swift/GlobalShortcuts.swift new file mode 100644 index 0000000..3bfa681 --- /dev/null +++ b/Source/Swift/GlobalShortcuts.swift @@ -0,0 +1,30 @@ +import Foundation +import MASShortcut + +/// Registers global keyboard shortcuts using `MASShortcutBinder`. +/// +/// Replaces the old `GlobalHotkeys` ObjC class that used the Carbon Event Manager directly. +/// Call `GlobalShortcuts.shared` once at app launch to register all hotkeys. +@objc final class GlobalShortcuts: NSObject { + @objc static let shared = GlobalShortcuts() + + private override init() { + super.init() + guard let binder = MASShortcutBinder.shared() else { return } + binder.bindingOptions = [ + NSBindingOption.valueTransformerName.rawValue: MASDictionaryTransformerName + ] + + let nc = NotificationCenter.default + // Notification names from Gas_Mask_Prefix.pch lines 42–44 (not importable in Swift) + binder.bindShortcut(withDefaultsKey: ActivatePreviousFilePrefKey) { + nc.post(name: .init("activatePreviousFileNotification"), object: nil) + } + binder.bindShortcut(withDefaultsKey: ActivateNextFilePrefKey) { + nc.post(name: .init("activateNextFileNotification"), object: nil) + } + binder.bindShortcut(withDefaultsKey: UpdateAndSynchronizePrefKey) { + nc.post(name: .init("updateAndSynchronizeNotification"), object: nil) + } + } +} diff --git a/Source/Swift/PreferencesView.swift b/Source/Swift/PreferencesView.swift index af98f6a..c221c6e 100644 --- a/Source/Swift/PreferencesView.swift +++ b/Source/Swift/PreferencesView.swift @@ -66,28 +66,24 @@ struct RemoteTab: View { struct HotkeysTab: View { var body: some View { - // Keys match ObjC #define constants in Preferences.h: - // ActivatePreviousFilePrefKey = @"activatePreviousHotkey" - // ActivateNextFilePrefKey = @"activateNextHotkey" - // UpdateAndSynchronizePrefKey = @"updateAndSynchronizeHotkey" Form { HStack { Text("Activate Previous File:") .frame(width: 160, alignment: .trailing) - ShortcutRecorderView(prefsKey: "activatePreviousHotkey") - .frame(width: 150, height: 22) + ShortcutRecorderView(prefsKey: ActivatePreviousFilePrefKey) + .frame(width: 150, height: 25) } HStack { Text("Activate Next File:") .frame(width: 160, alignment: .trailing) - ShortcutRecorderView(prefsKey: "activateNextHotkey") - .frame(width: 150, height: 22) + ShortcutRecorderView(prefsKey: ActivateNextFilePrefKey) + .frame(width: 150, height: 25) } HStack { Text("Update Remote Files:") .frame(width: 160, alignment: .trailing) - ShortcutRecorderView(prefsKey: "updateAndSynchronizeHotkey") - .frame(width: 150, height: 22) + ShortcutRecorderView(prefsKey: UpdateAndSynchronizePrefKey) + .frame(width: 150, height: 25) } } .padding(20) diff --git a/Source/Swift/RemoteIntervalMapper.swift b/Source/Swift/RemoteIntervalMapper.swift index d562e71..a3567dd 100644 --- a/Source/Swift/RemoteIntervalMapper.swift +++ b/Source/Swift/RemoteIntervalMapper.swift @@ -3,7 +3,7 @@ import Foundation /// Maps slider positions (1–9) to remote update intervals in minutes. /// /// Extracted from the ObjC `PreferenceController (Remote)` category for testability. -/// The same minute values are stored in UserDefaults and read by `GlobalHotkeys`. +/// The same minute values are stored in UserDefaults and used by Preferences. enum RemoteIntervalMapper { static let intervals: [(position: Int, minutes: Int)] = [ (1, 5), diff --git a/Source/Swift/ShortcutRecorderView.swift b/Source/Swift/ShortcutRecorderView.swift index 4e0187b..e2ea9e2 100644 --- a/Source/Swift/ShortcutRecorderView.swift +++ b/Source/Swift/ShortcutRecorderView.swift @@ -1,73 +1,20 @@ import AppKit -import ShortcutRecorder +import MASShortcut import SwiftUI -/// Wraps `RecorderControl` (ShortcutRecorder) for use in SwiftUI. +/// Wraps `MASShortcutView` for use in SwiftUI. /// /// Each instance reads/writes a single UserDefaults key (e.g. `"activatePreviousHotkey"`) -/// in the same plist dict format that `GlobalHotkeys` observes via KVO. +/// in the same plist dict format that `MASShortcutBinder` uses for global hotkey registration. struct ShortcutRecorderView: NSViewRepresentable { - /// The UserDefaults key storing the hotkey dict (matches ObjC #define constants). let prefsKey: String - func makeCoordinator() -> Coordinator { - Coordinator(prefsKey: prefsKey) + func makeNSView(context: Context) -> MASShortcutView { + let view = MASShortcutView() + view.setAssociatedUserDefaultsKey(prefsKey, withTransformerName: MASDictionaryTransformerName) + view.style = .texturedRect + return view } - func makeNSView(context: Context) -> RecorderControl { - let control = RecorderControl() - control.delegate = context.coordinator - - // Load existing shortcut from UserDefaults - if let plist = UserDefaults.standard.object(forKey: prefsKey), - let hotkey = Hotkey(plistRepresentation: plist), - hotkey.keyCode > 0 { - if let keyCode = KeyCode(rawValue: UInt16(hotkey.keyCode)) { - control.objectValue = Shortcut( - code: keyCode, - modifierFlags: Self.carbonToCocoaFlags(UInt32(hotkey.modifiers)), - characters: nil, - charactersIgnoringModifiers: nil - ) - } - } - - return control - } - - func updateNSView(_ nsView: RecorderControl, context: Context) { - // No-op: prefsKey is immutable per instance, and state changes are driven - // by user interaction within RecorderControl + its delegate. - } - - /// Convert Carbon modifier flags to Cocoa NSEvent.ModifierFlags. - private static func carbonToCocoaFlags(_ carbonFlags: UInt32) -> NSEvent.ModifierFlags { - var flags = NSEvent.ModifierFlags() - if carbonFlags & UInt32(cmdKey) != 0 { flags.insert(.command) } - if carbonFlags & UInt32(optionKey) != 0 { flags.insert(.option) } - if carbonFlags & UInt32(controlKey) != 0 { flags.insert(.control) } - if carbonFlags & UInt32(shiftKey) != 0 { flags.insert(.shift) } - return flags - } - - final class Coordinator: NSObject, RecorderControlDelegate { - let prefsKey: String - - init(prefsKey: String) { - self.prefsKey = prefsKey - } - - func recorderControlDidEndRecording(_ recorder: RecorderControl) { - let hotkey: Hotkey - if let shortcut = recorder.objectValue { - hotkey = Hotkey( - keyCode: Int32(shortcut.carbonKeyCode), - modifiers: Int32(shortcut.carbonModifierFlags) - ) - } else { - hotkey = Hotkey(keyCode: -1, modifiers: -1) - } - UserDefaults.standard.set(hotkey.plistRepresentation(), forKey: prefsKey) - } - } + func updateNSView(_ nsView: MASShortcutView, context: Context) {} } diff --git a/Tests/GasMaskTests/GlobalShortcutsTests.swift b/Tests/GasMaskTests/GlobalShortcutsTests.swift new file mode 100644 index 0000000..4297208 --- /dev/null +++ b/Tests/GasMaskTests/GlobalShortcutsTests.swift @@ -0,0 +1,78 @@ +import XCTest +import MASShortcut +@testable import Gas_Mask + +final class GlobalShortcutsTests: XCTestCase { + + func testShared_returnsSameInstance() { + let a = GlobalShortcuts.shared + let b = GlobalShortcuts.shared + XCTAssertTrue(a === b, "GlobalShortcuts.shared should return the same instance") + } + + func testShared_isNotNil() { + XCTAssertNotNil(GlobalShortcuts.shared) + } + + func testBinder_hasBindingOptionsSet() { + _ = GlobalShortcuts.shared + let binder = MASShortcutBinder.shared() + XCTAssertNotNil(binder) + let options = binder?.bindingOptions + XCTAssertNotNil(options, "Binder should have bindingOptions set") + let transformerName = options?[NSBindingOption.valueTransformerName.rawValue] as? String + XCTAssertEqual(transformerName, MASDictionaryTransformerName, + "Binder should use MASDictionaryTransformerName") + } + + func testActivatePreviousFile_registersShortcut() { + _ = GlobalShortcuts.shared + assertShortcutRegistered(forDefaultsKey: ActivatePreviousFilePrefKey) + } + + func testActivateNextFile_registersShortcut() { + _ = GlobalShortcuts.shared + assertShortcutRegistered(forDefaultsKey: ActivateNextFilePrefKey) + } + + func testUpdateAndSynchronize_registersShortcut() { + _ = GlobalShortcuts.shared + assertShortcutRegistered(forDefaultsKey: UpdateAndSynchronizePrefKey) + } + + // MARK: - Helpers + + /// Writes a shortcut to UserDefaults, waits for MASShortcutBinder to pick + /// it up via KVO, and verifies the shortcut is registered with the monitor. + private func assertShortcutRegistered( + forDefaultsKey defaultsKey: String, + file: StaticString = #file, + line: UInt = #line + ) { + guard let binder = MASShortcutBinder.shared() else { + XCTFail("MASShortcutBinder.shared() returned nil", file: file, line: line) + return + } + + // Create a test shortcut (Ctrl+Shift+Cmd+F5) and write to UserDefaults + // using the same dictionary transformer the binder expects. + let shortcut = MASShortcut(keyCode: Int(kVK_F5), + modifierFlags: [.control, .shift, .command]) + let transformer = ValueTransformer(forName: .init(rawValue: MASDictionaryTransformerName)) + let encoded = transformer?.reverseTransformedValue(shortcut) + UserDefaults.standard.set(encoded, forKey: defaultsKey) + + // Give KVO a chance to propagate. + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + // Verify the monitor registered the shortcut. + let registered = binder.shortcutMonitor.isShortcutRegistered(shortcut) + XCTAssertTrue(registered, + "Shortcut should be registered for key '\(defaultsKey)'", + file: file, line: line) + + // Clean up: remove the test shortcut to avoid side effects. + UserDefaults.standard.removeObject(forKey: defaultsKey) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.05)) + } +} diff --git a/Tests/GasMaskTests/ShortcutRecorderViewTests.swift b/Tests/GasMaskTests/ShortcutRecorderViewTests.swift new file mode 100644 index 0000000..8c61057 --- /dev/null +++ b/Tests/GasMaskTests/ShortcutRecorderViewTests.swift @@ -0,0 +1,82 @@ +import XCTest +import MASShortcut +@testable import Gas_Mask + +/// Tests for MASShortcutView configuration matching what ShortcutRecorderView sets up. +/// +/// NSViewRepresentable.Context cannot be constructed in unit tests, so we verify +/// the MASShortcutView configuration directly — this is the same code path that +/// ShortcutRecorderView.makeNSView executes. +final class ShortcutRecorderViewTests: XCTestCase { + + func testPrefsKeyConstants_matchExpectedValues() { + XCTAssertEqual(ActivatePreviousFilePrefKey, "activatePreviousHotkey") + XCTAssertEqual(ActivateNextFilePrefKey, "activateNextHotkey") + XCTAssertEqual(UpdateAndSynchronizePrefKey, "updateAndSynchronizeHotkey") + } + + func testView_configuredWithKeyAndStyle() { + let key = "testKey" + let view = MASShortcutView() + view.setAssociatedUserDefaultsKey(key, withTransformerName: MASDictionaryTransformerName) + view.style = .texturedRect + + XCTAssertEqual(view.associatedUserDefaultsKey, key) + XCTAssertEqual(view.style, .texturedRect) + } + + func testView_nilShortcutByDefault() { + let view = MASShortcutView() + XCTAssertNil(view.shortcutValue, "Fresh MASShortcutView should have nil shortcutValue") + } + + func testView_readsShortcutFromUserDefaults() { + let key = "testReadKey_\(UUID().uuidString)" + + // Write a shortcut to UserDefaults in dictionary format. + let shortcut = MASShortcut(keyCode: Int(kVK_F1), + modifierFlags: [.command, .shift]) + let transformer = ValueTransformer(forName: .init(rawValue: MASDictionaryTransformerName)) + let encoded = transformer?.reverseTransformedValue(shortcut) + UserDefaults.standard.set(encoded, forKey: key) + + let view = MASShortcutView() + view.setAssociatedUserDefaultsKey(key, withTransformerName: MASDictionaryTransformerName) + + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + XCTAssertNotNil(view.shortcutValue, "View should read shortcut from UserDefaults") + XCTAssertEqual(view.shortcutValue?.keyCode, Int(kVK_F1)) + + UserDefaults.standard.removeObject(forKey: key) + } + + func testView_clearingWritesEmptyDictionary() { + let key = "testClearKey_\(UUID().uuidString)" + + // Write a shortcut first. + let shortcut = MASShortcut(keyCode: Int(kVK_F2), + modifierFlags: [.command]) + let transformer = ValueTransformer(forName: .init(rawValue: MASDictionaryTransformerName)) + let encoded = transformer?.reverseTransformedValue(shortcut) + UserDefaults.standard.set(encoded, forKey: key) + + let view = MASShortcutView() + view.setAssociatedUserDefaultsKey(key, withTransformerName: MASDictionaryTransformerName) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + XCTAssertNotNil(view.shortcutValue, "Precondition: shortcut should be set") + + // Clear the shortcut. + view.shortcutValue = nil + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + // MASShortcutView writes an empty dictionary when cleared (via MASDictionaryTransformer). + // MASShortcutBinder treats this as "no shortcut" and unregisters the hotkey. + let stored = UserDefaults.standard.dictionary(forKey: key) + XCTAssertNotNil(stored, "Key should still exist after clearing") + XCTAssertTrue(stored?.isEmpty ?? false, + "Cleared shortcut should be stored as empty dictionary") + + UserDefaults.standard.removeObject(forKey: key) + } +}