From fde2dae146ea6653aa1c7ce450e63e8d047415d2 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Sat, 28 Feb 2026 12:24:35 +0200 Subject: [PATCH 1/3] feat: replace Preferences window with SwiftUI (#231) Migrate the Preferences window from ObjC/XIB to SwiftUI, following the same pattern used for URLSheet in PR #227. - Replace PreferenceController (ObjC) + Preferences.xib with a SwiftUI-based presenter using NSTabViewController for proper macOS toolbar-style tab icons - Add 5 SwiftUI tab views: General, Editor, Remote, Hotkeys, Update - Use SF Symbols for tab icons (gearshape, square.and.pencil, globe, command.square.fill, arrow.triangle.2.circlepath) - Bridge ShortcutRecorder via NSViewRepresentable for hotkey recording - Wrap SUUpdater and LoginItem in ObservableObject for SwiftUI binding - Add RemoteIntervalMapper for clean slider-to-minutes conversion - Remove unused preference icon PNGs and old ObjC files - Add PreferencesPresenter tests (window creation, reuse, tabs, icons) - Uniform tab content size prevents window resizing on tab switch --- Gas Mask.xcodeproj/project.pbxproj | 76 ++--- Preferences.xib | 237 --------------- Resources/Images/Preferences/Editor.png | Bin 5698 -> 0 bytes Resources/Images/Preferences/Hotkeys.png | Bin 6446 -> 0 bytes Resources/Images/Preferences/Remote.png | Bin 8105 -> 0 bytes Resources/Images/Preferences/Update.png | Bin 13034 -> 0 bytes Source/ApplicationController.h | 2 - Source/ApplicationController.m | 6 +- Source/PreferenceController.h | 42 --- Source/PreferenceController.m | 287 ------------------ Source/Swift/GasMask-Bridging-Header.h | 4 + Source/Swift/LoginItemObserver.swift | 19 ++ Source/Swift/PreferencesPresenter.swift | 46 +++ Source/Swift/PreferencesView.swift | 119 ++++++++ Source/Swift/RemoteIntervalMapper.swift | 35 +++ Source/Swift/ShortcutRecorderView.swift | 73 +++++ Source/Swift/SparkleObserver.swift | 54 ++++ Source/UpdateDateTransformer.h | 23 -- Source/UpdateDateTransformer.m | 55 ---- .../PreferencesPresenterTests.swift | 111 +++++++ .../RemoteIntervalMapperTests.swift | 41 +++ Tests/GasMaskTests/SparkleObserverTests.swift | 33 ++ docs/preferences-window.png | Bin 0 -> 56159 bytes 23 files changed, 578 insertions(+), 685 deletions(-) delete mode 100644 Preferences.xib delete mode 100644 Resources/Images/Preferences/Editor.png delete mode 100644 Resources/Images/Preferences/Hotkeys.png delete mode 100644 Resources/Images/Preferences/Remote.png delete mode 100644 Resources/Images/Preferences/Update.png delete mode 100644 Source/PreferenceController.h delete mode 100644 Source/PreferenceController.m create mode 100644 Source/Swift/LoginItemObserver.swift create mode 100644 Source/Swift/PreferencesPresenter.swift create mode 100644 Source/Swift/PreferencesView.swift create mode 100644 Source/Swift/RemoteIntervalMapper.swift create mode 100644 Source/Swift/ShortcutRecorderView.swift create mode 100644 Source/Swift/SparkleObserver.swift delete mode 100644 Source/UpdateDateTransformer.h delete mode 100644 Source/UpdateDateTransformer.m create mode 100644 Tests/GasMaskTests/PreferencesPresenterTests.swift create mode 100644 Tests/GasMaskTests/RemoteIntervalMapperTests.swift create mode 100644 Tests/GasMaskTests/SparkleObserverTests.swift create mode 100644 docs/preferences-window.png diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index 5bfba76..e3a5443 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -19,8 +19,7 @@ 351167C010D6346000A5FAA1 /* HostsGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 351167BF10D6346000A5FAA1 /* HostsGroup.m */; }; 3511697810D68B1000A5FAA1 /* FilesCountTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3511697710D68B1000A5FAA1 /* FilesCountTransformer.m */; }; 35116A7010D6AAD200A5FAA1 /* Menulet.m in Sources */ = {isa = PBXBuildFile; fileRef = 35116A6F10D6AAD200A5FAA1 /* Menulet.m */; }; - 35116AA010D6ADF000A5FAA1 /* Preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 35116A9F10D6ADF000A5FAA1 /* Preferences.xib */; }; - 35116AAD10D6AE5600A5FAA1 /* PreferenceController.m in Sources */ = {isa = PBXBuildFile; fileRef = 35116AAC10D6AE5600A5FAA1 /* PreferenceController.m */; }; + 35116A8510D6ABA000A5FAA1 /* menuIcon.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 35116A8410D6ABA000A5FAA1 /* menuIcon.tiff */; }; 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 */; }; @@ -73,7 +72,6 @@ 354E7F5E10AEFA9D00FC4757 /* PrivilegedActions.m in Sources */ = {isa = PBXBuildFile; fileRef = 354E7F5D10AEFA9D00FC4757 /* PrivilegedActions.m */; }; 354E7F8110AEFBC500FC4757 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 354E7F8010AEFBC500FC4757 /* Security.framework */; }; 354E80B510AF383C00FC4757 /* FileUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 354E80B410AF383C00FC4757 /* FileUtil.m */; }; - 3556CEF910D6B70100C7301E /* UpdateDateTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3556CEF810D6B70100C7301E /* UpdateDateTransformer.m */; }; 355F5C5B12244B67006C2884 /* LocalHostsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 355F5C5A12244B67006C2884 /* LocalHostsController.m */; }; 355F5CA512244F28006C2884 /* RemoteHostsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 355F5CA412244F28006C2884 /* RemoteHostsController.m */; }; 355F5CDF122452BE006C2884 /* AbstractHostsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 355F5CDE122452BE006C2884 /* AbstractHostsController.m */; }; @@ -103,10 +101,6 @@ 3597135D110DED0F00C7ECAF /* HostsMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 3597135C110DED0F00C7ECAF /* HostsMenu.m */; }; 359967541656B52500BCF16D /* NotificationHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 359967531656B52500BCF16D /* NotificationHelper.m */; }; 35A4CD301534927F005176BD /* Combined Hosts Hint.png in Resources */ = {isa = PBXBuildFile; fileRef = 35A4CD2F1534927F005176BD /* Combined Hosts Hint.png */; }; - 35B049921A46234100EB89CA /* Editor.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B0498E1A46234100EB89CA /* Editor.png */; }; - 35B049931A46234100EB89CA /* Hotkeys.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B0498F1A46234100EB89CA /* Hotkeys.png */; }; - 35B049941A46234100EB89CA /* Remote.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B049901A46234100EB89CA /* Remote.png */; }; - 35B049951A46234100EB89CA /* Update.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B049911A46234100EB89CA /* Update.png */; }; 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 */; }; @@ -145,6 +139,15 @@ AA00000C000000000000AAAA /* URLValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000B000000000000AAAA /* URLValidatorTests.swift */; }; AA00000E000000000000AAAA /* URLSheetPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */; }; 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 */; }; + 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 */; }; + BB00000C000000000000BBBB /* PreferencesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB00000B000000000000BBBB /* PreferencesPresenter.swift */; }; + 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 */; }; /* Begin PBXContainerItemProxy section */ 353D18A01114C067005C4E54 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -203,9 +206,7 @@ 3511697710D68B1000A5FAA1 /* FilesCountTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FilesCountTransformer.m; path = Source/FilesCountTransformer.m; sourceTree = ""; }; 35116A6E10D6AAD200A5FAA1 /* Menulet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Menulet.h; path = Source/Menulet.h; sourceTree = ""; }; 35116A6F10D6AAD200A5FAA1 /* Menulet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Menulet.m; path = Source/Menulet.m; sourceTree = ""; }; - 35116A9F10D6ADF000A5FAA1 /* Preferences.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Preferences.xib; sourceTree = ""; }; - 35116AAB10D6AE5600A5FAA1 /* PreferenceController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = PreferenceController.h; path = Source/PreferenceController.h; sourceTree = ""; }; - 35116AAC10D6AE5600A5FAA1 /* PreferenceController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PreferenceController.m; path = Source/PreferenceController.m; sourceTree = ""; }; + 35116A8410D6ABA000A5FAA1 /* menuIcon.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = menuIcon.tiff; path = Resources/Images/menuIcon.tiff; 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 = ""; }; @@ -283,8 +284,6 @@ 354E7F8010AEFBC500FC4757 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = /System/Library/Frameworks/Security.framework; sourceTree = ""; }; 354E80B310AF383C00FC4757 /* FileUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = FileUtil.h; path = Source/FileUtil.h; sourceTree = ""; }; 354E80B410AF383C00FC4757 /* FileUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FileUtil.m; path = Source/FileUtil.m; sourceTree = ""; }; - 3556CEF710D6B70100C7301E /* UpdateDateTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = UpdateDateTransformer.h; path = Source/UpdateDateTransformer.h; sourceTree = ""; }; - 3556CEF810D6B70100C7301E /* UpdateDateTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = UpdateDateTransformer.m; path = Source/UpdateDateTransformer.m; sourceTree = ""; }; 355F5C5912244B67006C2884 /* LocalHostsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LocalHostsController.h; path = Source/LocalHostsController.h; sourceTree = ""; }; 355F5C5A12244B67006C2884 /* LocalHostsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = LocalHostsController.m; path = Source/LocalHostsController.m; sourceTree = ""; }; 355F5CA312244F28006C2884 /* RemoteHostsController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RemoteHostsController.h; path = Source/RemoteHostsController.h; sourceTree = ""; }; @@ -333,11 +332,17 @@ AA00000B000000000000AAAA /* URLValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLValidatorTests.swift; sourceTree = ""; }; AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetPresenterTests.swift; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; + BB00000B000000000000BBBB /* PreferencesPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesPresenter.swift; path = "Source/Swift/PreferencesPresenter.swift"; sourceTree = ""; }; + 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 = ""; }; + 35A183A71A0ACF37002D6289 /* menuIcon@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "menuIcon@2x.tiff"; path = "Resources/Images/menuIcon@2x.tiff"; sourceTree = ""; }; 35A4CD2F1534927F005176BD /* Combined Hosts Hint.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Combined Hosts Hint.png"; path = "Resources/Images/Combined Hosts Hint.png"; sourceTree = ""; }; - 35B0498E1A46234100EB89CA /* Editor.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Editor.png; path = Resources/Images/Preferences/Editor.png; sourceTree = ""; }; - 35B0498F1A46234100EB89CA /* Hotkeys.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Hotkeys.png; path = Resources/Images/Preferences/Hotkeys.png; sourceTree = ""; }; - 35B049901A46234100EB89CA /* Remote.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Remote.png; path = Resources/Images/Preferences/Remote.png; sourceTree = ""; }; - 35B049911A46234100EB89CA /* Update.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = Update.png; path = Resources/Images/Preferences/Update.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 = ""; }; @@ -562,8 +567,6 @@ 3556CEF410D6B6F300C7301E /* GUI */, 353A80B910B01C050005CAD1 /* Preferences.h */, 353A80BA10B01C050005CAD1 /* Preferences.m */, - 35116AAB10D6AE5600A5FAA1 /* PreferenceController.h */, - 35116AAC10D6AE5600A5FAA1 /* PreferenceController.m */, ); name = Preferences; sourceTree = ""; @@ -668,7 +671,6 @@ 3545DC8E10E38F1800EBA66D /* Images */ = { isa = PBXGroup; children = ( - 35B0498D1A46233600EB89CA /* Preferences */, 35A4CD2F1534927F005176BD /* Combined Hosts Hint.png */, 358C36E31A0D6F2E00161C98 /* Combined_File_yosemite.tiff */, 358C36E71A0D6FCC00161C98 /* Combined_File_yosemite@2x.tiff */, @@ -707,9 +709,6 @@ 3556CEF410D6B6F300C7301E /* GUI */ = { isa = PBXGroup; children = ( - 35116A9F10D6ADF000A5FAA1 /* Preferences.xib */, - 3556CEF710D6B70100C7301E /* UpdateDateTransformer.h */, - 3556CEF810D6B70100C7301E /* UpdateDateTransformer.m */, ); name = GUI; sourceTree = ""; @@ -771,17 +770,20 @@ AA000005000000000000AAAA /* NetworkStatusObserver.swift */, AA000007000000000000AAAA /* URLSheetView.swift */, AA000009000000000000AAAA /* URLSheetPresenter.swift */, + BB000020000000000000BBBB /* Preferences */, ); name = Swift; sourceTree = ""; }; - 35B0498D1A46233600EB89CA /* Preferences */ = { + BB000020000000000000BBBB /* Preferences */ = { isa = PBXGroup; children = ( - 35B0498E1A46234100EB89CA /* Editor.png */, - 35B0498F1A46234100EB89CA /* Hotkeys.png */, - 35B049901A46234100EB89CA /* Remote.png */, - 35B049911A46234100EB89CA /* Update.png */, + BB000009000000000000BBBB /* PreferencesView.swift */, + BB00000B000000000000BBBB /* PreferencesPresenter.swift */, + BB000001000000000000BBBB /* RemoteIntervalMapper.swift */, + BB000003000000000000BBBB /* ShortcutRecorderView.swift */, + BB000005000000000000BBBB /* SparkleObserver.swift */, + BB000007000000000000BBBB /* LoginItemObserver.swift */, ); name = Preferences; sourceTree = ""; @@ -862,6 +864,9 @@ AA00000B000000000000AAAA /* URLValidatorTests.swift */, AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */, AA00000F000000000000AAAA /* URLSheetViewTests.swift */, + BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */, + BB00000F000000000000BBBB /* SparkleObserverTests.swift */, + BB000011000000000000BBBB /* PreferencesPresenterTests.swift */, CC2B3C4D5E6F000100000010 /* NodeTests.m */, CC2B3C4D5E6F000100000011 /* HostsTests.m */, CC2B3C4D5E6F000100000012 /* HostsGroupTests.m */, @@ -990,8 +995,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 35B049921A46234100EB89CA /* Editor.png in Resources */, - 35B049931A46234100EB89CA /* Hotkeys.png in Resources */, 3579F1181114B95300123416 /* Launcher.app in Resources */, 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */, 1DDD58160DA1D0A300B32029 /* MainMenu.xib in Resources */, @@ -999,7 +1002,6 @@ 352E04FD106281940071E25B /* Create.png in Resources */, 352E04FF106281940071E25B /* Save.png in Resources */, 358E0A891A3A1E1A004521D1 /* Remote yosemite@2x.tiff in Resources */, - 35B049941A46234100EB89CA /* Remote.png in Resources */, 352E05B610628D790071E25B /* Activated.png in Resources */, 350C7F3B1A3C56BB00B46B09 /* Read Only@2x.png in Resources */, 352E05B710628D790071E25B /* Blue Dot.png in Resources */, @@ -1007,7 +1009,7 @@ 350C7F391A3C55B000B46B09 /* Create@2x.png in Resources */, 353A80FC10B020B10005CAD1 /* UserDefaults.plist in Resources */, 358E0A881A3A1E1A004521D1 /* Remote yosemite.tiff in Resources */, - 35116AA010D6ADF000A5FAA1 /* Preferences.xib in Resources */, + 35116A8510D6ABA000A5FAA1 /* menuIcon.tiff in Resources */, 35D83BEF20DD97CF00169358 /* Assets.xcassets in Resources */, 352F48D210DA90FE000003BE /* default.hst in Resources */, 3545DC7310E38DDE00EBA66D /* Offline.png in Resources */, @@ -1037,7 +1039,6 @@ 35A4CD301534927F005176BD /* Combined Hosts Hint.png in Resources */, 359289AB1659222E00492494 /* CHANGELOG.txt in Resources */, 358E0A841A3A1C54004521D1 /* Local File yosemite.tiff in Resources */, - 35B049951A46234100EB89CA /* Update.png in Resources */, 358C36E61A0D6F2E00161C98 /* Combined_File.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1082,10 +1083,8 @@ 351167C010D6346000A5FAA1 /* HostsGroup.m in Sources */, 3511697810D68B1000A5FAA1 /* FilesCountTransformer.m in Sources */, 35116A7010D6AAD200A5FAA1 /* Menulet.m in Sources */, - 35116AAD10D6AE5600A5FAA1 /* PreferenceController.m in Sources */, 35116AC010D6AF0500A5FAA1 /* LoginItem.m in Sources */, 35116ADA10D6B00200A5FAA1 /* Hotkey.m in Sources */, - 3556CEF910D6B70100C7301E /* UpdateDateTransformer.m in Sources */, 358448B310DFE283002E6A9B /* Util.m in Sources */, 3545DCA910E396CF00EBA66D /* Network.m in Sources */, 3541CE0510E4AF9B00FA00CB /* SyncingArrowsBadge.m in Sources */, @@ -1106,6 +1105,12 @@ AA000006000000000000AAAA /* NetworkStatusObserver.swift in Sources */, AA000008000000000000AAAA /* URLSheetView.swift in Sources */, AA00000A000000000000AAAA /* URLSheetPresenter.swift in Sources */, + BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */, + BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */, + BB000006000000000000BBBB /* SparkleObserver.swift in Sources */, + BB000008000000000000BBBB /* LoginItemObserver.swift in Sources */, + BB00000A000000000000BBBB /* PreferencesView.swift in Sources */, + BB00000C000000000000BBBB /* PreferencesPresenter.swift in Sources */, 356DB76A1824EAFD0020CEA0 /* ExtendedNSSplitView.m in Sources */, 35E9008A1147F42900851A25 /* MAAttachedWindow.m in Sources */, 350E7D3E121093E400D2F5F5 /* AlertBadge.m in Sources */, @@ -1147,6 +1152,9 @@ AA00000C000000000000AAAA /* URLValidatorTests.swift in Sources */, AA00000E000000000000AAAA /* URLSheetPresenterTests.swift in Sources */, AA000010000000000000AAAA /* URLSheetViewTests.swift in Sources */, + BB00000E000000000000BBBB /* RemoteIntervalMapperTests.swift in Sources */, + BB000010000000000000BBBB /* SparkleObserverTests.swift in Sources */, + BB000012000000000000BBBB /* PreferencesPresenterTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Preferences.xib b/Preferences.xib deleted file mode 100644 index 7a8f185..0000000 --- a/Preferences.xib +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - UpdateDateTransformer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 5 15 30 1h 2h 5h 10h 24h 7d - - - - - - - - - diff --git a/Resources/Images/Preferences/Editor.png b/Resources/Images/Preferences/Editor.png deleted file mode 100644 index 707527e93d9f6dd23ffc64acef366acbb01b2ba4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5698 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 diff --git a/Resources/Images/Preferences/Hotkeys.png b/Resources/Images/Preferences/Hotkeys.png deleted file mode 100644 index db3d6d84b8d9f99e236cb4dbb8303e8ba2fd0c9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6446 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 { - @private - IBOutlet NSView *generalView, *editorView, *hotkeysView, *updateView, *remoteView; - LoginItem *loginItem; - - __unsafe_unretained IBOutlet NSButton *showHostFileNameButton; - - // Remote - IBOutlet NSSlider *remoteIntervalSlider; - NSDictionary *remoteIntervals; - - // Hotkeys - IBOutlet SRRecorderControl *activatePreviousHotkey, *activateNextHotkey, *updateHotkey; -} - -- (void) setPreferenceView:(id)sender; - -@end diff --git a/Source/PreferenceController.m b/Source/PreferenceController.m deleted file mode 100644 index d8e2ce3..0000000 --- a/Source/PreferenceController.m +++ /dev/null @@ -1,287 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2012 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 "PreferenceController.h" -#import "Preferences.h" -#import "Preferences+Remote.h" -#import "LoginItem.h" -#import "Hotkey.h" -#import "Util.h" - -#define TOOLBAR_GENERAL @"TOOLBAR_GENERAL" -#define TOOLBAR_EDITOR @"TOOLBAR_EDITOR" -#define TOOLBAR_REMOTE @"TOOLBAR_REMOTE" -#define TOOLBAR_HOTKEYS @"TOOLBAR_HOTKEYS" -#define TOOLBAR_UPDATE @"TOOLBAR_UPDATE" - - -@interface PreferenceController (Remote) -- (void)initRemote; -- (int)remoteInterval; -- (void)setRemoteInterval:(int)interval; -@end - -@interface PreferenceController (Hotkeys) -- (void)initHotkeys; -@end - -@interface PreferenceController (General) -- (void)initGeneral; -@end - -@implementation PreferenceController - -- (id)init -{ - self = [super initWithWindowNibName:@"Preferences"]; - if (self == nil) { - return nil; - } - - NSToolbar * toolbar = [[NSToolbar alloc] initWithIdentifier: @"Preferences Toolbar"]; - [toolbar setDelegate: self]; - [toolbar setAllowsUserCustomization: NO]; - [toolbar setDisplayMode: NSToolbarDisplayModeIconAndLabel]; - [toolbar setSizeMode: NSToolbarSizeModeRegular]; - [toolbar setSelectedItemIdentifier: TOOLBAR_GENERAL]; - [[self window] setToolbar: toolbar]; - - return self; -} - -- (void) awakeFromNib -{ - [self setPreferenceView:nil]; - - loginItem = [LoginItem new]; - [loginItem bind:@"enabled" toObject:[Preferences instance] withKeyPath:@"values.openAtLogin" options:nil]; - - [self initGeneral]; - [self initRemote]; - [self initHotkeys]; -} - -- (NSArray *) toolbarSelectableItemIdentifiers: (NSToolbar *) toolbar -{ - return [self toolbarDefaultItemIdentifiers: toolbar]; -} - -- (NSArray *) toolbarDefaultItemIdentifiers: (NSToolbar *) toolbar -{ - return [self toolbarAllowedItemIdentifiers: toolbar]; -} - -- (NSArray *) toolbarAllowedItemIdentifiers: (NSToolbar *) toolbar -{ - return [NSArray arrayWithObjects: TOOLBAR_GENERAL, - TOOLBAR_EDITOR, - TOOLBAR_REMOTE, - TOOLBAR_HOTKEYS, - TOOLBAR_UPDATE, - nil]; -} - -- (NSToolbarItem *) toolbar: (NSToolbar *) toolbar itemForItemIdentifier: (NSString *) ident willBeInsertedIntoToolbar: (BOOL) flag -{ - NSToolbarItem * item = [[NSToolbarItem alloc] initWithItemIdentifier: ident]; - - if ([ident isEqualTo:TOOLBAR_GENERAL]) { - [item setLabel: @"General"]; - [item setImage: [NSImage imageNamed: NSImageNamePreferencesGeneral]]; - } - else if ([ident isEqualTo:TOOLBAR_EDITOR]) { - [item setLabel: @"Editor"]; - [item setImage: [NSImage imageNamed: @"Editor.png"]]; - } - else if ([ident isEqualTo:TOOLBAR_REMOTE]) { - [item setLabel: @"Remote"]; - [item setImage: [NSImage imageNamed: @"Remote.png"]]; - } - else if ([ident isEqualTo:TOOLBAR_HOTKEYS]) { - [item setLabel: @"Hotkeys"]; - [item setImage: [NSImage imageNamed: @"Hotkeys.png"]]; - } - else if ([ident isEqualTo:TOOLBAR_UPDATE]) { - [item setLabel: @"Update"]; - [item setImage: [NSImage imageNamed: @"Update.png"]]; - } - [item setTarget: self]; - [item setAction: @selector(setPreferenceView:)]; - [item setAutovalidates: NO]; - - return item; -} - -- (void) setPreferenceView:(id)sender -{ - NSView *view = generalView; - NSString * identifier = [sender itemIdentifier]; - if ([identifier isEqualToString:TOOLBAR_EDITOR]) { - view = editorView; - } - else if ([identifier isEqualToString:TOOLBAR_REMOTE]) { - view = remoteView; - } - else if ([identifier isEqualToString:TOOLBAR_HOTKEYS]) { - view = hotkeysView; - } - else if ([identifier isEqualToString:TOOLBAR_UPDATE]) { - view = updateView; - } - - NSWindow * window = [self window]; - NSRect windowRect = [window frame]; - float difference = ([view frame].size.height - [[window contentView] frame].size.height); - windowRect.origin.y -= difference; - windowRect.size.height += difference; - - [window setContentView: view]; - [window setFrame: windowRect display: YES animate: YES]; -} - -@end - -@implementation PreferenceController (General) -/** - * OS X 10.10 and later support the NSStatusItemBar button which is what the - * "Show Host File Name in Status Bar" feature is built upon. So if we're - * not 10.10 or above, then we need to disable the preference selection. - */ -- (void) initGeneral -{ - showHostFileNameButton.enabled = ![Util isPre10_10]; -} -@end - -@implementation PreferenceController (Remote) - -- (void)initRemote -{ - NSArray *objects = [NSArray arrayWithObjects: - [NSNumber numberWithInt:5], - [NSNumber numberWithInt:15], - [NSNumber numberWithInt:30], - [NSNumber numberWithInt:60], - [NSNumber numberWithInt:120], - [NSNumber numberWithInt:300], - [NSNumber numberWithInt:600], - [NSNumber numberWithInt:1440], - [NSNumber numberWithInt:10080], - nil]; - NSArray *keys = [NSArray arrayWithObjects: - [NSNumber numberWithInt:1], - [NSNumber numberWithInt:2], - [NSNumber numberWithInt:3], - [NSNumber numberWithInt:4], - [NSNumber numberWithInt:5], - [NSNumber numberWithInt:6], - [NSNumber numberWithInt:7], - [NSNumber numberWithInt:8], - [NSNumber numberWithInt:9], - nil]; - - remoteIntervals = [NSDictionary dictionaryWithObjects:objects forKeys:keys]; - - [remoteIntervalSlider bind:@"value" toObject:self withKeyPath:@"remoteInterval" options:nil]; -} - -- (int)remoteInterval -{ - NSNumber *interval = [NSNumber numberWithInt:[Preferences remoteHostsUpdateInterval]]; - - for (NSNumber *key in remoteIntervals) { - if ([[remoteIntervals objectForKey:key] isEqual:interval]) { - return [key integerValue]; - } - } - - return 0; -} - -- (void)setRemoteInterval:(int)interval -{ - NSNumber *value = [remoteIntervals objectForKey:[NSNumber numberWithInt:interval]]; - [Preferences setRemoteHostsUpdateInterval:[value intValue]]; -} - -@end - - -@implementation PreferenceController (Hotkeys) - -- (void)initHotkeys -{ - id plist = [[[Preferences instance] defaults] valueForKey:ActivatePreviousFilePrefKey]; - Hotkey *hotkey = [[Hotkey alloc] initWithPlistRepresentation:plist]; - if (hotkey.keyCode > 0) { - [activatePreviousHotkey setObjectValue:[SRShortcut shortcutWithCode:(SRKeyCode)hotkey.keyCode - modifierFlags:SRCarbonToCocoaFlags((UInt32)hotkey.modifiers) - characters:nil - charactersIgnoringModifiers:nil]]; - } - - plist = [[[Preferences instance] defaults] valueForKey:ActivateNextFilePrefKey]; - hotkey = [[Hotkey alloc] initWithPlistRepresentation:plist]; - if (hotkey.keyCode > 0) { - [activateNextHotkey setObjectValue:[SRShortcut shortcutWithCode:(SRKeyCode)hotkey.keyCode - modifierFlags:SRCarbonToCocoaFlags((UInt32)hotkey.modifiers) - characters:nil - charactersIgnoringModifiers:nil]]; - } - - plist = [[[Preferences instance] defaults] valueForKey:UpdateAndSynchronizePrefKey]; - hotkey = [[Hotkey alloc] initWithPlistRepresentation:plist]; - if (hotkey.keyCode > 0) { - [updateHotkey setObjectValue:[SRShortcut shortcutWithCode:(SRKeyCode)hotkey.keyCode - modifierFlags:SRCarbonToCocoaFlags((UInt32)hotkey.modifiers) - characters:nil - charactersIgnoringModifiers:nil]]; - } -} - -- (void)recorderControlDidEndRecording:(SRRecorderControl *)aControl -{ - SRShortcut *shortcut = aControl.objectValue; - Hotkey *hotkey; - if (shortcut) { - hotkey = [[Hotkey alloc] initWithKeyCode:(int)shortcut.carbonKeyCode - modifiers:(int)shortcut.carbonModifierFlags]; - } else { - hotkey = [[Hotkey alloc] initWithKeyCode:-1 modifiers:-1]; - } - - NSString *prefKey; - if (aControl == activatePreviousHotkey) { - prefKey = ActivatePreviousFilePrefKey; - } - else if (aControl == activateNextHotkey) { - prefKey = ActivateNextFilePrefKey; - } - else { - prefKey = UpdateAndSynchronizePrefKey; - } - - [[[Preferences instance] defaults] setValue:[hotkey plistRepresentation] forKey:prefKey]; -} - - -@end \ No newline at end of file diff --git a/Source/Swift/GasMask-Bridging-Header.h b/Source/Swift/GasMask-Bridging-Header.h index 4e8bda7..d6b3ce2 100644 --- a/Source/Swift/GasMask-Bridging-Header.h +++ b/Source/Swift/GasMask-Bridging-Header.h @@ -2,3 +2,7 @@ #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/LoginItemObserver.swift b/Source/Swift/LoginItemObserver.swift new file mode 100644 index 0000000..5b58834 --- /dev/null +++ b/Source/Swift/LoginItemObserver.swift @@ -0,0 +1,19 @@ +import Foundation + +/// Thin ObservableObject wrapper around the existing ObjC `LoginItem` class. +/// +/// Delegates all `SMAppService` logic to `LoginItem`, avoiding a parallel implementation. +final class LoginItemObserver: ObservableObject { + private let loginItem = LoginItem() + + @Published var isEnabled: Bool { + didSet { + guard isEnabled != oldValue else { return } + loginItem.setEnabled(isEnabled) + } + } + + init() { + self.isEnabled = loginItem.enabled() + } +} diff --git a/Source/Swift/PreferencesPresenter.swift b/Source/Swift/PreferencesPresenter.swift new file mode 100644 index 0000000..ad61ed3 --- /dev/null +++ b/Source/Swift/PreferencesPresenter.swift @@ -0,0 +1,46 @@ +import AppKit +import SwiftUI + +@objc final class PreferencesPresenter: NSObject { + private static var window: NSWindow? + + @objc static func showPreferences() { + if let existing = window { + existing.makeKeyAndOrderFront(nil) + return + } + + let tabVC = NSTabViewController() + tabVC.tabStyle = .toolbar + + let contentSize = NSSize(width: 550, height: 150) + + func makeTab(label: String, symbolName: String, rootView: V) -> NSTabViewItem { + let hc = NSHostingController(rootView: rootView) + hc.preferredContentSize = contentSize + let item = NSTabViewItem(viewController: hc) + item.label = label + item.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: label) + return item + } + + tabVC.addTabViewItem(makeTab(label: "General", symbolName: "gearshape", rootView: GeneralTab())) + tabVC.addTabViewItem(makeTab(label: "Editor", symbolName: "square.and.pencil", rootView: EditorTab())) + tabVC.addTabViewItem(makeTab(label: "Remote", symbolName: "globe", rootView: RemoteTab())) + tabVC.addTabViewItem(makeTab(label: "Hotkeys", symbolName: "command.square.fill", rootView: HotkeysTab())) + tabVC.addTabViewItem(makeTab(label: "Update", symbolName: "arrow.triangle.2.circlepath", rootView: UpdateTab())) + + let w = NSWindow(contentViewController: tabVC) + w.styleMask = [.titled, .closable] + w.toolbarStyle = .preference + w.title = tabVC.tabViewItems.first?.label ?? "Preferences" + w.setFrameAutosaveName("PreferencesWindow") + + // Keep the window alive after close so it can be reopened + w.isReleasedWhenClosed = false + + window = w + w.center() + w.makeKeyAndOrderFront(nil) + } +} diff --git a/Source/Swift/PreferencesView.swift b/Source/Swift/PreferencesView.swift new file mode 100644 index 0000000..af98f6a --- /dev/null +++ b/Source/Swift/PreferencesView.swift @@ -0,0 +1,119 @@ +import SwiftUI + +// MARK: - General Tab + +struct GeneralTab: View { + @StateObject private var loginItemObserver = LoginItemObserver() + @AppStorage("overrideExternalModifications") private var overrideExternalModifications = true + @AppStorage("showNameInStatusBar") private var showNameInStatusBar = false + + var body: some View { + Form { + Toggle("Open at Login", isOn: $loginItemObserver.isEnabled) + Toggle("Override external modifications", isOn: $overrideExternalModifications) + Toggle("Show Host File Name in Status Bar", isOn: $showNameInStatusBar) + } + .padding(20) + } +} + +// MARK: - Editor Tab + +struct EditorTab: View { + @AppStorage("syntaxHighlighting") private var syntaxHighlighting = true + + var body: some View { + Form { + Toggle("Syntax Highlighting", isOn: $syntaxHighlighting) + } + .padding(20) + } +} + +// MARK: - Remote Tab + +struct RemoteTab: View { + @State private var sliderPosition: Double + + init() { + let currentMinutes = Int(Preferences.remoteHostsUpdateInterval()) + _sliderPosition = State(initialValue: Double(RemoteIntervalMapper.position(forMinutes: currentMinutes))) + } + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Text("Update interval:") + + Slider(value: $sliderPosition, in: 1...9, step: 1) + .onChange(of: sliderPosition) { newValue in + let minutes = RemoteIntervalMapper.minutes(forPosition: Int(newValue)) + Preferences.setRemoteHostsUpdateInterval(Int32(minutes)) + } + + HStack(spacing: 0) { + ForEach(Array(RemoteIntervalMapper.labels.enumerated()), id: \.offset) { _, label in + Text(label) + .font(.caption) + .frame(maxWidth: .infinity) + } + } + } + .padding(20) + } +} + +// MARK: - Hotkeys Tab + +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) + } + HStack { + Text("Activate Next File:") + .frame(width: 160, alignment: .trailing) + ShortcutRecorderView(prefsKey: "activateNextHotkey") + .frame(width: 150, height: 22) + } + HStack { + Text("Update Remote Files:") + .frame(width: 160, alignment: .trailing) + ShortcutRecorderView(prefsKey: "updateAndSynchronizeHotkey") + .frame(width: 150, height: 22) + } + } + .padding(20) + } +} + +// MARK: - Update Tab + +struct UpdateTab: View { + @StateObject private var sparkleObserver = SparkleObserver() + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + Toggle("Automatically check for updates", isOn: Binding( + get: { sparkleObserver.automaticChecksEnabled }, + set: { sparkleObserver.setAutomaticChecks($0) } + )) + + Text(sparkleObserver.lastCheckDateFormatted) + .font(.caption) + + Button("Check Now") { + sparkleObserver.checkForUpdates() + } + .disabled(!sparkleObserver.automaticChecksEnabled) + } + .padding(20) + } +} diff --git a/Source/Swift/RemoteIntervalMapper.swift b/Source/Swift/RemoteIntervalMapper.swift new file mode 100644 index 0000000..d562e71 --- /dev/null +++ b/Source/Swift/RemoteIntervalMapper.swift @@ -0,0 +1,35 @@ +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`. +enum RemoteIntervalMapper { + static let intervals: [(position: Int, minutes: Int)] = [ + (1, 5), + (2, 15), + (3, 30), + (4, 60), + (5, 120), + (6, 300), + (7, 600), + (8, 1440), + (9, 10080), + ] + + /// Display labels for each slider tick mark, matching the 9 positions. + static let labels: [String] = ["5m", "15m", "30m", "1h", "2h", "5h", "10h", "24h", "7d"] + + /// Returns the interval in minutes for a given slider position (1–9). + static func minutes(forPosition position: Int) -> Int { + intervals.first { $0.position == position }?.minutes ?? 5 + } + + /// Returns the slider position (1–9) for a given interval in minutes. + /// + /// Returns `1` for unknown values — an intentional improvement over the ObjC code + /// which returned `0` (below the slider's minimum), causing an impossible state. + static func position(forMinutes minutes: Int) -> Int { + intervals.first { $0.minutes == minutes }?.position ?? 1 + } +} diff --git a/Source/Swift/ShortcutRecorderView.swift b/Source/Swift/ShortcutRecorderView.swift new file mode 100644 index 0000000..4e0187b --- /dev/null +++ b/Source/Swift/ShortcutRecorderView.swift @@ -0,0 +1,73 @@ +import AppKit +import ShortcutRecorder +import SwiftUI + +/// Wraps `RecorderControl` (ShortcutRecorder) 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. +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) -> 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) + } + } +} diff --git a/Source/Swift/SparkleObserver.swift b/Source/Swift/SparkleObserver.swift new file mode 100644 index 0000000..9810ac2 --- /dev/null +++ b/Source/Swift/SparkleObserver.swift @@ -0,0 +1,54 @@ +import Foundation +import Sparkle + +/// Wraps `SUUpdater.shared()` as an `ObservableObject` for SwiftUI. +/// +/// Replaces the XIB-instantiated `SUUpdater` object and `UpdateDateTransformer`. +final class SparkleObserver: ObservableObject { + @Published var lastCheckDate: Date? + @Published var automaticChecksEnabled: Bool + + private var dateObservation: NSKeyValueObservation? + + private static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .short + return formatter + }() + + /// Formats `lastCheckDate` as `"Last Checked: Never"` or `"Last Checked: "`. + var lastCheckDateFormatted: String { + guard let date = lastCheckDate else { + return "Last Checked: Never" + } + return "Last Checked: \(Self.dateFormatter.string(from: date))" + } + + init() { + guard let updater = SUUpdater.shared() else { + self.lastCheckDate = nil + self.automaticChecksEnabled = false + return + } + self.lastCheckDate = updater.lastUpdateCheckDate + self.automaticChecksEnabled = UserDefaults.standard.bool(forKey: "SUEnableAutomaticChecks") + + // Observe lastUpdateCheckDate via KVO + dateObservation = updater.observe(\.lastUpdateCheckDate, options: [.new]) { [weak self] _, change in + DispatchQueue.main.async { + self?.lastCheckDate = change.newValue ?? nil + } + } + } + + /// Writes only to UserDefaults — SUUpdater observes this key via its own KVO. + func setAutomaticChecks(_ enabled: Bool) { + automaticChecksEnabled = enabled + UserDefaults.standard.set(enabled, forKey: "SUEnableAutomaticChecks") + } + + func checkForUpdates() { + SUUpdater.shared()?.checkForUpdates(nil) + } +} diff --git a/Source/UpdateDateTransformer.h b/Source/UpdateDateTransformer.h deleted file mode 100644 index 8875935..0000000 --- a/Source/UpdateDateTransformer.h +++ /dev/null @@ -1,23 +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 UpdateDateTransformer : NSValueTransformer - -@end diff --git a/Source/UpdateDateTransformer.m b/Source/UpdateDateTransformer.m deleted file mode 100644 index 6a9ab43..0000000 --- a/Source/UpdateDateTransformer.m +++ /dev/null @@ -1,55 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2012 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 "UpdateDateTransformer.h" - - -@implementation UpdateDateTransformer - -+ (Class)transformedValueClass -{ - return [NSString class]; -} - -+ (BOOL)allowsReverseTransformation -{ - return NO; -} - -- (id)transformedValue:(id)value -{ - - NSString *checked; - - if (value == nil) { - checked = @"Never"; - } - else { - NSDate *date = (NSDate*)value; - NSDateFormatter *formatter = [NSDateFormatter new]; - [formatter setDateStyle:NSDateFormatterLongStyle]; - [formatter setTimeStyle:NSDateFormatterShortStyle]; - checked = [formatter stringFromDate:date]; - } - - return [NSString stringWithFormat:@"Last Checked: %@", checked]; -} - -@end diff --git a/Tests/GasMaskTests/PreferencesPresenterTests.swift b/Tests/GasMaskTests/PreferencesPresenterTests.swift new file mode 100644 index 0000000..05e83e4 --- /dev/null +++ b/Tests/GasMaskTests/PreferencesPresenterTests.swift @@ -0,0 +1,111 @@ +import XCTest +import AppKit +@testable import Gas_Mask + +final class PreferencesPresenterTests: XCTestCase { + + /// Finds the preferences window by its `NSTabViewController` content. + private func preferencesWindow() -> NSWindow? { + NSApp.windows.first { $0.contentViewController is NSTabViewController } + } + + override func tearDown() { + // Close any preferences window opened during the test. + // Drain CA transactions first to avoid animation crashes. + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + if let w = preferencesWindow() { w.close() } + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + super.tearDown() + } + + func testShowPreferences_createsWindow() { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let w = preferencesWindow() + XCTAssertNotNil(w, "A preferences window should exist") + XCTAssertTrue(w?.isVisible ?? false, "Preferences window should be visible") + } + + func testShowPreferences_reusesWindow() { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let first = preferencesWindow() + XCTAssertNotNil(first) + + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + let second = preferencesWindow() + XCTAssertTrue(first === second, "Should reuse the same window instance") + } + + func testShowPreferences_hasFiveTabs() { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let tabVC = preferencesWindow()?.contentViewController as? NSTabViewController + XCTAssertEqual(tabVC?.tabViewItems.count, 5, "Should have 5 preference tabs") + } + + func testShowPreferences_tabLabelsMatch() { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let tabVC = preferencesWindow()?.contentViewController as? NSTabViewController + let labels = tabVC?.tabViewItems.map(\.label) + XCTAssertEqual(labels, ["General", "Editor", "Remote", "Hotkeys", "Update"]) + } + + func testShowPreferences_tabsHaveIcons() { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let tabVC = preferencesWindow()?.contentViewController as? NSTabViewController + XCTAssertNotNil(tabVC) + for item in tabVC?.tabViewItems ?? [] { + XCTAssertNotNil(item.image, "Tab '\(item.label)' should have an icon") + } + } + + /// Captures a screenshot of a specific tab and saves to /tmp/preferences-.png. + private func captureTab(index: Int, name: String) throws { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + let w = try XCTUnwrap(preferencesWindow()) + let tabVC = try XCTUnwrap(w.contentViewController as? NSTabViewController) + tabVC.selectedTabViewItemIndex = index + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let frame = w.frame + let screenFrame = w.screen?.frame ?? NSScreen.main!.frame + let cgRect = CGRect( + x: frame.origin.x, + y: screenFrame.height - frame.origin.y - frame.height, + width: frame.width, + height: frame.height + ) + if let displayID = w.screen?.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID, + let image = CGDisplayCreateImage(displayID, rect: cgRect) { + let bitmap = NSBitmapImageRep(cgImage: image) + if let png = bitmap.representation(using: .png, properties: [:]) { + try png.write(to: URL(fileURLWithPath: "/tmp/preferences-\(name).png")) + } + } + } + + func testScreenshot_allTabs() throws { + for (i, name) in ["general", "editor", "remote", "hotkeys", "update"].enumerated() { + try captureTab(index: i, name: name) + } + } + + func testShowPreferences_toolbarStyleIsPreference() { + PreferencesPresenter.showPreferences() + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + let w = preferencesWindow() + XCTAssertEqual(w?.toolbarStyle, .preference) + } +} diff --git a/Tests/GasMaskTests/RemoteIntervalMapperTests.swift b/Tests/GasMaskTests/RemoteIntervalMapperTests.swift new file mode 100644 index 0000000..abf7a0b --- /dev/null +++ b/Tests/GasMaskTests/RemoteIntervalMapperTests.swift @@ -0,0 +1,41 @@ +import XCTest +@testable import Gas_Mask + +final class RemoteIntervalMapperTests: XCTestCase { + + func testAllForwardMappings() { + let expected: [(position: Int, minutes: Int)] = [ + (1, 5), (2, 15), (3, 30), (4, 60), (5, 120), + (6, 300), (7, 600), (8, 1440), (9, 10080), + ] + for (position, minutes) in expected { + XCTAssertEqual( + RemoteIntervalMapper.minutes(forPosition: position), minutes, + "Position \(position) should map to \(minutes) minutes" + ) + } + } + + func testAllReverseMappings() { + let expected: [(minutes: Int, position: Int)] = [ + (5, 1), (15, 2), (30, 3), (60, 4), (120, 5), + (300, 6), (600, 7), (1440, 8), (10080, 9), + ] + for (minutes, position) in expected { + XCTAssertEqual( + RemoteIntervalMapper.position(forMinutes: minutes), position, + "\(minutes) minutes should map to position \(position)" + ) + } + } + + func testUnknownMinutes_fallsBackToPosition1() { + XCTAssertEqual(RemoteIntervalMapper.position(forMinutes: 999), 1) + XCTAssertEqual(RemoteIntervalMapper.position(forMinutes: 0), 1) + XCTAssertEqual(RemoteIntervalMapper.position(forMinutes: -1), 1) + } + + func testLabels_hasNineElements() { + XCTAssertEqual(RemoteIntervalMapper.labels.count, 9) + } +} diff --git a/Tests/GasMaskTests/SparkleObserverTests.swift b/Tests/GasMaskTests/SparkleObserverTests.swift new file mode 100644 index 0000000..fd44774 --- /dev/null +++ b/Tests/GasMaskTests/SparkleObserverTests.swift @@ -0,0 +1,33 @@ +import XCTest +@testable import Gas_Mask + +final class SparkleObserverTests: XCTestCase { + + func testLastCheckDateFormatted_nil_returnsNever() { + let observer = SparkleObserver() + observer.lastCheckDate = nil + XCTAssertEqual(observer.lastCheckDateFormatted, "Last Checked: Never") + } + + func testLastCheckDateFormatted_date_returnsFormattedString() { + let observer = SparkleObserver() + // Use a fixed date: 2026-02-28 15:45:00 UTC + var components = DateComponents() + components.year = 2026 + components.month = 2 + components.day = 28 + components.hour = 15 + components.minute = 45 + let date = Calendar.current.date(from: components)! + observer.lastCheckDate = date + + let result = observer.lastCheckDateFormatted + XCTAssertTrue(result.hasPrefix("Last Checked: "), + "Should start with 'Last Checked: ', got: \(result)") + XCTAssertFalse(result.hasSuffix("Never"), + "Should not say Never when date is set") + // The exact format depends on locale, so just verify it contains the year + XCTAssertTrue(result.contains("2026"), + "Should contain the year, got: \(result)") + } +} diff --git a/docs/preferences-window.png b/docs/preferences-window.png new file mode 100644 index 0000000000000000000000000000000000000000..2fb7423dd2ce712caa42121f2549ff6d2766e94a GIT binary patch literal 56159 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 F{~srB=W+l5 literal 0 HcmV?d00001 From 47f3b7b5d75552b91ed26309e2d715a4d02971ea Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Sat, 28 Feb 2026 12:33:40 +0200 Subject: [PATCH 2/3] fix: address code review findings for Preferences SwiftUI migration - Use Sparkle public API for reading automaticChecksEnabled instead of hardcoding the UserDefaults key - Guard screenshot test against headless CI with XCTSkipIf and remove NSScreen.main force unwrap --- Source/Swift/SparkleObserver.swift | 2 +- Tests/GasMaskTests/PreferencesPresenterTests.swift | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Swift/SparkleObserver.swift b/Source/Swift/SparkleObserver.swift index 9810ac2..66ee4c1 100644 --- a/Source/Swift/SparkleObserver.swift +++ b/Source/Swift/SparkleObserver.swift @@ -32,7 +32,7 @@ final class SparkleObserver: ObservableObject { return } self.lastCheckDate = updater.lastUpdateCheckDate - self.automaticChecksEnabled = UserDefaults.standard.bool(forKey: "SUEnableAutomaticChecks") + self.automaticChecksEnabled = updater.automaticallyChecksForUpdates // Observe lastUpdateCheckDate via KVO dateObservation = updater.observe(\.lastUpdateCheckDate, options: [.new]) { [weak self] _, change in diff --git a/Tests/GasMaskTests/PreferencesPresenterTests.swift b/Tests/GasMaskTests/PreferencesPresenterTests.swift index 05e83e4..ef18930 100644 --- a/Tests/GasMaskTests/PreferencesPresenterTests.swift +++ b/Tests/GasMaskTests/PreferencesPresenterTests.swift @@ -71,6 +71,7 @@ final class PreferencesPresenterTests: XCTestCase { /// Captures a screenshot of a specific tab and saves to /tmp/preferences-.png. private func captureTab(index: Int, name: String) throws { + try XCTSkipIf(NSScreen.main == nil, "No display available") PreferencesPresenter.showPreferences() RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) let w = try XCTUnwrap(preferencesWindow()) @@ -79,7 +80,7 @@ final class PreferencesPresenterTests: XCTestCase { RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) let frame = w.frame - let screenFrame = w.screen?.frame ?? NSScreen.main!.frame + let screenFrame = w.screen?.frame ?? NSScreen.main?.frame ?? .zero let cgRect = CGRect( x: frame.origin.x, y: screenFrame.height - frame.origin.y - frame.height, From 15106c495dfd0b8fff614a4c9c4c0cc7ce631ee3 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Sat, 28 Feb 2026 12:51:54 +0200 Subject: [PATCH 3/3] fix: remove stale menuIcon.tiff references from pbxproj The rebase onto master picked up menuIcon.tiff references that were deleted by #232 (app icon migration to asset catalog). Remove the orphaned PBXBuildFile, PBXFileReference, and Resources build phase entries for menuIcon.tiff and menuIcon@2x.tiff. --- Gas Mask.xcodeproj/project.pbxproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index e3a5443..3b1ff49 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 351167C010D6346000A5FAA1 /* HostsGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 351167BF10D6346000A5FAA1 /* HostsGroup.m */; }; 3511697810D68B1000A5FAA1 /* FilesCountTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3511697710D68B1000A5FAA1 /* FilesCountTransformer.m */; }; 35116A7010D6AAD200A5FAA1 /* Menulet.m in Sources */ = {isa = PBXBuildFile; fileRef = 35116A6F10D6AAD200A5FAA1 /* Menulet.m */; }; - 35116A8510D6ABA000A5FAA1 /* menuIcon.tiff in Resources */ = {isa = PBXBuildFile; fileRef = 35116A8410D6ABA000A5FAA1 /* menuIcon.tiff */; }; 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 */; }; @@ -206,7 +205,6 @@ 3511697710D68B1000A5FAA1 /* FilesCountTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FilesCountTransformer.m; path = Source/FilesCountTransformer.m; sourceTree = ""; }; 35116A6E10D6AAD200A5FAA1 /* Menulet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Menulet.h; path = Source/Menulet.h; sourceTree = ""; }; 35116A6F10D6AAD200A5FAA1 /* Menulet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Menulet.m; path = Source/Menulet.m; sourceTree = ""; }; - 35116A8410D6ABA000A5FAA1 /* menuIcon.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = menuIcon.tiff; path = Resources/Images/menuIcon.tiff; 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 = ""; }; @@ -341,7 +339,6 @@ 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 = ""; }; - 35A183A71A0ACF37002D6289 /* menuIcon@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "menuIcon@2x.tiff"; path = "Resources/Images/menuIcon@2x.tiff"; 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 = ""; }; @@ -1009,7 +1006,6 @@ 350C7F391A3C55B000B46B09 /* Create@2x.png in Resources */, 353A80FC10B020B10005CAD1 /* UserDefaults.plist in Resources */, 358E0A881A3A1E1A004521D1 /* Remote yosemite.tiff in Resources */, - 35116A8510D6ABA000A5FAA1 /* menuIcon.tiff in Resources */, 35D83BEF20DD97CF00169358 /* Assets.xcassets in Resources */, 352F48D210DA90FE000003BE /* default.hst in Resources */, 3545DC7310E38DDE00EBA66D /* Offline.png in Resources */,