diff --git a/.swiftlint.yml b/.swiftlint.yml index 045da4e3..f60e17d0 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -6,6 +6,7 @@ disabled_rules: - file_length - function_body_length - function_parameter_count + - generic_type_name - identifier_name - large_tuple - line_length diff --git a/Ice.xcodeproj/project.pbxproj b/Ice.xcodeproj/project.pbxproj index ea47b78c..e69810c2 100644 --- a/Ice.xcodeproj/project.pbxproj +++ b/Ice.xcodeproj/project.pbxproj @@ -3,277 +3,43 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ 170423D92B56DE78004A2549 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 170423D82B56DE78004A2549 /* Sparkle */; }; - 170423DD2B56E77D004A2549 /* UpdatesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170423DC2B56E77D004A2549 /* UpdatesManager.swift */; }; - 170749C82B12078F009DDF73 /* GlobalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170749C72B12078F009DDF73 /* GlobalEventMonitor.swift */; }; - 170749CA2B1207D9009DDF73 /* UniversalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170749C92B1207D9009DDF73 /* UniversalEventMonitor.swift */; }; - 170CF88E2B0EDD780073F982 /* AnyLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170CF88D2B0EDD780073F982 /* AnyLocalizedError.swift */; }; - 170CF8902B101D980073F982 /* ColorStop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170CF88F2B101D980073F982 /* ColorStop.swift */; }; - 171C6F982C0353CE0097A5C8 /* MenuBarAppearanceEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171C6F972C0353CE0097A5C8 /* MenuBarAppearanceEditor.swift */; }; - 171C6F9C2C0356BC0097A5C8 /* GeneralSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171C6F9B2C0356BC0097A5C8 /* GeneralSettingsPane.swift */; }; - 171C6FC62C0571510097A5C8 /* Bundle+copyrightString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171C6FC52C0571510097A5C8 /* Bundle+copyrightString.swift */; }; - 171C6FC92C0659340097A5C8 /* Task+timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171C6FC82C0659340097A5C8 /* Task+timeout.swift */; }; - 1725FC6A2AED973800A59081 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1725FC692AED973800A59081 /* AppState.swift */; }; - 172778482C1490EA00F6F687 /* EventTap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172778472C1490EA00F6F687 /* EventTap.swift */; }; - 1727786E2C15181300F6F687 /* MenuBarItemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1727786D2C15181300F6F687 /* MenuBarItemManager.swift */; }; - 1736F77C2ADBBF340073428E /* CustomGradientPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1736F77B2ADBBF340073428E /* CustomGradientPicker.swift */; }; - 1736F7802ADBC02B0073428E /* CustomGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1736F77F2ADBC02B0073428E /* CustomGradient.swift */; }; - 1737E3E52BA3C4AA009F0EFA /* HotkeyAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1737E3E42BA3C4AA009F0EFA /* HotkeyAction.swift */; }; - 173C24892B8E80830096F7A1 /* AboutSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173C24882B8E80830096F7A1 /* AboutSettingsPane.swift */; }; - 173C248C2B8E821C0096F7A1 /* UpdatesSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173C248B2B8E821C0096F7A1 /* UpdatesSettingsPane.swift */; }; - 174AA5D62B71D97100E3FE74 /* MenuBarOverlayPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174AA5D52B71D97100E3FE74 /* MenuBarOverlayPanel.swift */; }; - 174D4E242BA1D28700D11129 /* HotkeyRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D4E232BA1D28700D11129 /* HotkeyRecorder.swift */; }; - 174D4E262BA1D35D00D11129 /* HotkeyRecorderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D4E252BA1D35D00D11129 /* HotkeyRecorderModel.swift */; }; - 174D4E282BA2089100D11129 /* Hotkey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D4E272BA2089100D11129 /* Hotkey.swift */; }; - 174D4E2A2BA230FB00D11129 /* HotkeySettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 174D4E292BA230FB00D11129 /* HotkeySettingsManager.swift */; }; 175061912B1543DD003144CD /* LaunchAtLogin in Frameworks */ = {isa = PBXBuildFile; productRef = 175061902B1543DD003144CD /* LaunchAtLogin */; }; - 17540BD82B20C0DA00A0F965 /* NSBezierPath+drawShadow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17540BD72B20C0DA00A0F965 /* NSBezierPath+drawShadow.swift */; }; - 17540BDC2B23BD5700A0F965 /* NSBezierPath+intersects.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17540BDB2B23BD5700A0F965 /* NSBezierPath+intersects.swift */; }; - 17543EB12C2C7A100052711E /* RunLoopLocalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17543EB02C2C7A100052711E /* RunLoopLocalEventMonitor.swift */; }; - 175812542B80FBFA00D622DA /* MenuBarAppearanceEditorPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175812532B80FBFA00D622DA /* MenuBarAppearanceEditorPanel.swift */; }; - 175812592B82ACA900D622DA /* Annotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 175812582B82ACA800D622DA /* Annotation.swift */; }; - 176B23F42ADB76A1008AE86B /* CustomColorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176B23F32ADB76A1008AE86B /* CustomColorPicker.swift */; }; - 177354662B1B8502001CF731 /* MenuBarShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177354652B1B8502001CF731 /* MenuBarShape.swift */; }; - 1773546C2B1BBBA1001CF731 /* MenuBarShapePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1773546B2B1BBBA1001CF731 /* MenuBarShapePicker.swift */; }; - 177354842B1F9AF9001CF731 /* NSBezierPath+union.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177354832B1F9AF9001CF731 /* NSBezierPath+union.swift */; }; - 177386F32B092A0700448BBF /* ControlItemImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177386F22B092A0700448BBF /* ControlItemImage.swift */; }; - 177386F52B0A654D00448BBF /* ControlItemImageSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177386F42B0A654D00448BBF /* ControlItemImageSet.swift */; }; 1787C4272B16890B002F50DF /* AXSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 1787C4262B16890B002F50DF /* AXSwift */; }; - 1787C42C2B16AE0B002F50DF /* Permission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1787C42B2B16AE0B002F50DF /* Permission.swift */; }; - 1787C4302B16AE78002F50DF /* PermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1787C42F2B16AE78002F50DF /* PermissionsManager.swift */; }; - 1787C4342B16AECF002F50DF /* PermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1787C4332B16AECF002F50DF /* PermissionsView.swift */; }; - 1787C4362B17A9EB002F50DF /* Acknowledgements.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 1787C4352B17A9EB002F50DF /* Acknowledgements.pdf */; }; - 1787C43B2B187187002F50DF /* MenuBarTintKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1787C43A2B187187002F50DF /* MenuBarTintKind.swift */; }; - 17928F1A2AC5DF9C0016C615 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17928F192AC5DF9C0016C615 /* Defaults.swift */; }; - 179AC2FC2C1146EF0051E7B0 /* RemoveSidebarToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179AC2FB2C1146EF0051E7B0 /* RemoveSidebarToggle.swift */; }; - 179AC2FF2C11475F0051E7B0 /* NSSplitViewItem+swizzledCanCollapse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179AC2FE2C11475F0051E7B0 /* NSSplitViewItem+swizzledCanCollapse.swift */; }; - 179F13BA2B90D34800EC6B52 /* MenuBarAppearanceConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179F13B92B90D34800EC6B52 /* MenuBarAppearanceConfiguration.swift */; }; - 17AB36322BA081D800F50C0F /* NSImage+resized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17AB36312BA081D800F50C0F /* NSImage+resized.swift */; }; - 17B331D02B9916E70084EBB0 /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B331CF2B9916E70084EBB0 /* SettingsManager.swift */; }; - 17B331D32B991D290084EBB0 /* GeneralSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B331D22B991D290084EBB0 /* GeneralSettingsManager.swift */; }; - 17B331D52B991D8D0084EBB0 /* AdvancedSettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B331D42B991D8D0084EBB0 /* AdvancedSettingsManager.swift */; }; - 17B380F32ADCBC8A0002C9C3 /* OnKeyDown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B380F22ADCBC8A0002C9C3 /* OnKeyDown.swift */; }; - 17B380F52ADCBE090002C9C3 /* LocalEventMonitorModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B380F42ADCBE090002C9C3 /* LocalEventMonitorModifier.swift */; }; - 17B7F32B2B264C1800CDCF49 /* MenuBarAppearanceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B7F32A2B264C1800CDCF49 /* MenuBarAppearanceManager.swift */; }; - 17BE3DD02C1A45B3008B98EF /* IceBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BE3DCF2C1A45B3008B98EF /* IceBar.swift */; }; - 17BE3DDB2C1A63B6008B98EF /* LayoutBarStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BE3DDA2C1A63B6008B98EF /* LayoutBarStyle.swift */; }; - 17BE3DE02C1A705E008B98EF /* IceBarPanel+hasKeyAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BE3DDF2C1A705E008B98EF /* IceBarPanel+hasKeyAppearance.swift */; }; - 17BE3DE22C1ACAEA008B98EF /* Erased.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17BE3DE12C1ACAEA008B98EF /* Erased.swift */; }; - 17C298B82BD42C390074DEAC /* Predicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C298B72BD42C390074DEAC /* Predicates.swift */; }; - 17C303782BA62CD20079755B /* MigrationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C303772BA62CD20079755B /* MigrationManager.swift */; }; - 17C3C5F72B75A36100B9648C /* NSScreen+displayID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C3C5F62B75A36100B9648C /* NSScreen+displayID.swift */; }; - 17CC22B02B8A0CA6001A0582 /* HotkeysSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CC22AF2B8A0CA6001A0582 /* HotkeysSettingsPane.swift */; }; - 17CC22B52B8A55E7001A0582 /* WindowInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CC22B42B8A55E7001A0582 /* WindowInfo.swift */; }; - 17CC22BC2B8CDE6F001A0582 /* EventManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CC22BB2B8CDE6F001A0582 /* EventManager.swift */; }; - 17CDCACD2C2E6F6A000B1CFF /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CDCACC2C2E6F6A000B1CFF /* Deprecated.swift */; }; - 17CDCACF2C2E6F77000B1CFF /* Private.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CDCACE2C2E6F77000B1CFF /* Private.swift */; }; - 17CDCAD12C2E6FB0000B1CFF /* Bridging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CDCAD02C2E6FB0000B1CFF /* Bridging.swift */; }; - 17CDCAD42C2E8A04000B1CFF /* CGError+logString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17CDCAD32C2E8A04000B1CFF /* CGError+logString.swift */; }; - 17D1AC8D2B97BB5900726180 /* MenuBarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D1AC8C2B97BB5900726180 /* MenuBarItem.swift */; }; - 17D1AC8F2B97E71D00726180 /* AdvancedSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D1AC8E2B97E71D00726180 /* AdvancedSettingsPane.swift */; }; - 17DFF4AC2AD5FB3300B5177A /* MenuBarAppearanceSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DFF4AB2AD5FB3300B5177A /* MenuBarAppearanceSettingsPane.swift */; }; - 17DFF4C02AD8DBC500B5177A /* CodableColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17DFF4BF2AD8DBC500B5177A /* CodableColor.swift */; }; - 17EC6B582AE0C34A0065F260 /* Comparable+clamped.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EC6B572AE0C34A0065F260 /* Comparable+clamped.swift */; }; - 17ECD4EE2B290F8D00E33147 /* PermissionsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17ECD4ED2B290F8D00E33147 /* PermissionsWindow.swift */; }; - 17EE695E2C26618F00F778A7 /* AnyInsettableShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EE695D2C26618F00F778A7 /* AnyInsettableShape.swift */; }; - 17EE69612C26A80300F778A7 /* CGColor+brightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EE69602C26A80300F778A7 /* CGColor+brightness.swift */; }; - 17EE69652C27777C00F778A7 /* MenuBarItemImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EE69642C27777C00F778A7 /* MenuBarItemImageCache.swift */; }; - 17EE69682C277B4E00F778A7 /* Publisher+mapToVoid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17EE69672C277B4E00F778A7 /* Publisher+mapToVoid.swift */; }; - 17F71BB22B87E37800905CBA /* RehideStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F71BB12B87E37800905CBA /* RehideStrategy.swift */; }; 17F71BB52B880B4500905CBA /* CompactSlider in Frameworks */ = {isa = PBXBuildFile; productRef = 17F71BB42B880B4500905CBA /* CompactSlider */; }; - 17F93D2C2C20FC440058C0AF /* Sequence+sortedByOrderInMenuBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F93D2B2C20FC440058C0AF /* Sequence+sortedByOrderInMenuBar.swift */; }; - 17F998322C15C91400D75EC0 /* MouseCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F998312C15C91400D75EC0 /* MouseCursor.swift */; }; - 17F9983C2C1655EF00D75EC0 /* LayoutBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F9983B2C1655EF00D75EC0 /* LayoutBarContainer.swift */; }; - 17F9983E2C16562300D75EC0 /* LayoutBarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F9983D2C16562300D75EC0 /* LayoutBarItemView.swift */; }; - 17F998412C16568600D75EC0 /* CGImage+trimmingTransparentPixels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F998402C16568600D75EC0 /* CGImage+trimmingTransparentPixels.swift */; }; - 17F998462C16C78000D75EC0 /* Injection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F998452C16C78000D75EC0 /* Injection.swift */; }; - 17F9984B2C16CBC100D75EC0 /* LayoutBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F9984A2C16CBC100D75EC0 /* LayoutBar.swift */; }; - 17F9984D2C16CD8E00D75EC0 /* MenuBarItemsSettingsPane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F9984C2C16CD8E00D75EC0 /* MenuBarItemsSettingsPane.swift */; }; - 17F9984F2C16CF8600D75EC0 /* LayoutBarPaddingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F9984E2C16CF8600D75EC0 /* LayoutBarPaddingView.swift */; }; - 17F998512C16D02E00D75EC0 /* LayoutBarScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F998502C16D02E00D75EC0 /* LayoutBarScrollView.swift */; }; - 17F998532C16EFCD00D75EC0 /* CGImage+averageColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F998522C16EFCD00D75EC0 /* CGImage+averageColor.swift */; }; - 17FE5AD32BCA7A7500E4F8D9 /* ScreenStateManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FE5AD22BCA7A7500E4F8D9 /* ScreenStateManager.swift */; }; - 17FE5AD52BCA9BE200E4F8D9 /* ScreenState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FE5AD42BCA9BE200E4F8D9 /* ScreenState.swift */; }; - 71008DF02AB907B00036B1F3 /* ObjectAssociation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71008DEF2AB907B00036B1F3 /* ObjectAssociation.swift */; }; - 7105CA212C5D2EFF004E439E /* StatusItemDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7105CA202C5D2EFF004E439E /* StatusItemDefaults.swift */; }; - 7105CA252C5D2F44004E439E /* StatusItemDefaultsKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7105CA242C5D2F44004E439E /* StatusItemDefaultsKey.swift */; }; - 711535F22AB9F6C1003193AD /* BindingExposable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711535F12AB9F6C1003193AD /* BindingExposable.swift */; }; - 711568B02C59ADBF00CDF58F /* NSScreen+frameOfNotch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711568AF2C59ADBF00CDF58F /* NSScreen+frameOfNotch.swift */; }; - 71287C602C3189AC0028706E /* IceBarColorManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71287C5F2C3189AC0028706E /* IceBarColorManager.swift */; }; - 71287C622C31D75F0028706E /* MenuBarAverageColorInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71287C612C31D75F0028706E /* MenuBarAverageColorInfo.swift */; }; - 71287C642C3233B30028706E /* NSScreen+hasNotch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71287C632C3233B30028706E /* NSScreen+hasNotch.swift */; }; - 7133ED5E2A853FCF000A7E1B /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED5D2A853FCF000A7E1B /* Constants.swift */; }; - 7133ED652A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED642A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift */; }; - 7133ED6A2A85870E000A7E1B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7133ED692A85870E000A7E1B /* SettingsView.swift */; }; - 7150A7AD2AA4265F0045EA68 /* KeyCombination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7AC2AA4265F0045EA68 /* KeyCombination.swift */; }; - 7150A7AF2AA426C60045EA68 /* HotkeyRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7AE2AA426C60045EA68 /* HotkeyRegistry.swift */; }; - 7150A7B12AA427F80045EA68 /* KeyCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7B02AA427F80045EA68 /* KeyCode.swift */; }; - 7150A7B32AA42B640045EA68 /* Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7150A7B22AA42B640045EA68 /* Modifiers.swift */; }; - 71553E372C378BF50083F5BE /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71553E362C378BF50083F5BE /* Notifications.swift */; }; - 71604D612AAE62DA002FE96F /* BottomBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71604D602AAE62DA002FE96F /* BottomBar.swift */; }; - 7162406F2AA0A323003EC671 /* MenuBarSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7162406E2AA0A323003EC671 /* MenuBarSection.swift */; }; - 7166832E2A767E6A006ABF84 /* IceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7166832D2A767E6A006ABF84 /* IceApp.swift */; }; - 716683322A767E6B006ABF84 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 716683312A767E6B006ABF84 /* Assets.xcassets */; }; - 7166833D2A7680CE006ABF84 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7166833C2A7680CE006ABF84 /* AppDelegate.swift */; }; - 7166834B2A76811C006ABF84 /* Logger+initWithCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716683452A76811C006ABF84 /* Logger+initWithCategory.swift */; }; - 7166834C2A76811C006ABF84 /* NSStatusItem+showMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716683472A76811C006ABF84 /* NSStatusItem+showMenu.swift */; }; - 716683502A7681AF006ABF84 /* MenuBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7166834F2A7681AF006ABF84 /* MenuBarManager.swift */; }; - 716683522A76820A006ABF84 /* ControlItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 716683512A76820A006ABF84 /* ControlItem.swift */; }; - 718152C32C34E595005564AA /* NSScreen+screenWithMouse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718152C22C34E595005564AA /* NSScreen+screenWithMouse.swift */; }; - 71829E0A2C2FDCC200503604 /* NSScreen+getMenuBarHeight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71829E092C2FDCC200503604 /* NSScreen+getMenuBarHeight.swift */; }; - 71839C162A9B6A0D00250044 /* OnFrameChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71839C152A9B6A0D00250044 /* OnFrameChange.swift */; }; - 718E3F782C679F680029B835 /* IceBarLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 718E3F772C679F680029B835 /* IceBarLocation.swift */; }; - 71C400322AAD76A8006FDB1C /* ReadWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71C400312AAD76A8006FDB1C /* ReadWindow.swift */; }; - 71D2B02E2AAFDD5C0002B6C8 /* Bundle+versionString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D2B02D2AAFDD5C0002B6C8 /* Bundle+versionString.swift */; }; - 71D36C4F2A88FE4900D89CD5 /* SettingsWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D36C4E2A88FE4900D89CD5 /* SettingsWindow.swift */; }; - 71F4280F2C3B5C870025706C /* SettingsNavigationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F4280E2C3B5C870025706C /* SettingsNavigationIdentifier.swift */; }; - 71F428112C3B5CE90025706C /* NavigationIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428102C3B5CE90025706C /* NavigationIdentifier.swift */; }; - 71F428132C3B5E030025706C /* IconResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428122C3B5E030025706C /* IconResource.swift */; }; - 71F428152C3B5F3D0025706C /* AppNavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71F428142C3B5F3D0025706C /* AppNavigationState.swift */; }; - 71FEA2542A8D701B0048341A /* LocalEventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71FEA2532A8D701B0048341A /* LocalEventMonitor.swift */; }; + 7127A9FF2C4886D100D99DEF /* IfritStatic in Frameworks */ = {isa = PBXBuildFile; productRef = 7127A9FE2C4886D100D99DEF /* IfritStatic */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 170423DC2B56E77D004A2549 /* UpdatesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesManager.swift; sourceTree = ""; }; - 170749C72B12078F009DDF73 /* GlobalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventMonitor.swift; sourceTree = ""; }; - 170749C92B1207D9009DDF73 /* UniversalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UniversalEventMonitor.swift; sourceTree = ""; }; - 170CF88D2B0EDD780073F982 /* AnyLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyLocalizedError.swift; sourceTree = ""; }; - 170CF88F2B101D980073F982 /* ColorStop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorStop.swift; sourceTree = ""; }; - 171C6F972C0353CE0097A5C8 /* MenuBarAppearanceEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAppearanceEditor.swift; sourceTree = ""; }; - 171C6F9B2C0356BC0097A5C8 /* GeneralSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsPane.swift; sourceTree = ""; }; - 171C6FC52C0571510097A5C8 /* Bundle+copyrightString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+copyrightString.swift"; sourceTree = ""; }; - 171C6FC82C0659340097A5C8 /* Task+timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+timeout.swift"; sourceTree = ""; }; - 1725FC692AED973800A59081 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; - 1726A3F82B3378B8008B09DD /* Acknowledgements.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Acknowledgements.rtf; sourceTree = ""; }; - 172778472C1490EA00F6F687 /* EventTap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTap.swift; sourceTree = ""; }; - 1727786D2C15181300F6F687 /* MenuBarItemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItemManager.swift; sourceTree = ""; }; - 1736F77B2ADBBF340073428E /* CustomGradientPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGradientPicker.swift; sourceTree = ""; }; - 1736F77F2ADBC02B0073428E /* CustomGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomGradient.swift; sourceTree = ""; }; - 1737E3E42BA3C4AA009F0EFA /* HotkeyAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyAction.swift; sourceTree = ""; }; - 173C24882B8E80830096F7A1 /* AboutSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsPane.swift; sourceTree = ""; }; - 173C248B2B8E821C0096F7A1 /* UpdatesSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatesSettingsPane.swift; sourceTree = ""; }; - 174AA5D52B71D97100E3FE74 /* MenuBarOverlayPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarOverlayPanel.swift; sourceTree = ""; }; - 174D4E232BA1D28700D11129 /* HotkeyRecorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorder.swift; sourceTree = ""; }; - 174D4E252BA1D35D00D11129 /* HotkeyRecorderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRecorderModel.swift; sourceTree = ""; }; - 174D4E272BA2089100D11129 /* Hotkey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hotkey.swift; sourceTree = ""; }; - 174D4E292BA230FB00D11129 /* HotkeySettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeySettingsManager.swift; sourceTree = ""; }; - 17540BD72B20C0DA00A0F965 /* NSBezierPath+drawShadow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBezierPath+drawShadow.swift"; sourceTree = ""; }; - 17540BDB2B23BD5700A0F965 /* NSBezierPath+intersects.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBezierPath+intersects.swift"; sourceTree = ""; }; - 17543EB02C2C7A100052711E /* RunLoopLocalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunLoopLocalEventMonitor.swift; sourceTree = ""; }; - 175812532B80FBFA00D622DA /* MenuBarAppearanceEditorPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAppearanceEditorPanel.swift; sourceTree = ""; }; - 175812582B82ACA800D622DA /* Annotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Annotation.swift; sourceTree = ""; }; - 176B23F32ADB76A1008AE86B /* CustomColorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomColorPicker.swift; sourceTree = ""; }; - 177354652B1B8502001CF731 /* MenuBarShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarShape.swift; sourceTree = ""; }; - 1773546B2B1BBBA1001CF731 /* MenuBarShapePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarShapePicker.swift; sourceTree = ""; }; - 177354832B1F9AF9001CF731 /* NSBezierPath+union.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBezierPath+union.swift"; sourceTree = ""; }; - 177386F22B092A0700448BBF /* ControlItemImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlItemImage.swift; sourceTree = ""; }; - 177386F42B0A654D00448BBF /* ControlItemImageSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlItemImageSet.swift; sourceTree = ""; }; - 1787C42B2B16AE0B002F50DF /* Permission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Permission.swift; sourceTree = ""; }; - 1787C42F2B16AE78002F50DF /* PermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsManager.swift; sourceTree = ""; }; - 1787C4332B16AECF002F50DF /* PermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsView.swift; sourceTree = ""; }; - 1787C4352B17A9EB002F50DF /* Acknowledgements.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = Acknowledgements.pdf; sourceTree = ""; }; - 1787C43A2B187187002F50DF /* MenuBarTintKind.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarTintKind.swift; sourceTree = ""; }; - 17928F192AC5DF9C0016C615 /* Defaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Defaults.swift; sourceTree = ""; }; - 179AC2FB2C1146EF0051E7B0 /* RemoveSidebarToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveSidebarToggle.swift; sourceTree = ""; }; - 179AC2FE2C11475F0051E7B0 /* NSSplitViewItem+swizzledCanCollapse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSSplitViewItem+swizzledCanCollapse.swift"; sourceTree = ""; }; - 179F13B92B90D34800EC6B52 /* MenuBarAppearanceConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAppearanceConfiguration.swift; sourceTree = ""; }; - 17AB36312BA081D800F50C0F /* NSImage+resized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+resized.swift"; sourceTree = ""; }; - 17B331CF2B9916E70084EBB0 /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; - 17B331D22B991D290084EBB0 /* GeneralSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsManager.swift; sourceTree = ""; }; - 17B331D42B991D8D0084EBB0 /* AdvancedSettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsManager.swift; sourceTree = ""; }; - 17B380F22ADCBC8A0002C9C3 /* OnKeyDown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnKeyDown.swift; sourceTree = ""; }; - 17B380F42ADCBE090002C9C3 /* LocalEventMonitorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalEventMonitorModifier.swift; sourceTree = ""; }; - 17B7F32A2B264C1800CDCF49 /* MenuBarAppearanceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAppearanceManager.swift; sourceTree = ""; }; - 17BE3DCF2C1A45B3008B98EF /* IceBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceBar.swift; sourceTree = ""; }; - 17BE3DDA2C1A63B6008B98EF /* LayoutBarStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutBarStyle.swift; sourceTree = ""; }; - 17BE3DDF2C1A705E008B98EF /* IceBarPanel+hasKeyAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IceBarPanel+hasKeyAppearance.swift"; sourceTree = ""; }; - 17BE3DE12C1ACAEA008B98EF /* Erased.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Erased.swift; sourceTree = ""; }; - 17C261E22B5AC03C0076F129 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 17C298B72BD42C390074DEAC /* Predicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Predicates.swift; sourceTree = ""; }; - 17C303772BA62CD20079755B /* MigrationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationManager.swift; sourceTree = ""; }; - 17C3C5F62B75A36100B9648C /* NSScreen+displayID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+displayID.swift"; sourceTree = ""; }; - 17CC22AF2B8A0CA6001A0582 /* HotkeysSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeysSettingsPane.swift; sourceTree = ""; }; - 17CC22B42B8A55E7001A0582 /* WindowInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowInfo.swift; sourceTree = ""; }; - 17CC22BB2B8CDE6F001A0582 /* EventManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventManager.swift; sourceTree = ""; }; - 17CDCACC2C2E6F6A000B1CFF /* Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deprecated.swift; sourceTree = ""; }; - 17CDCACE2C2E6F77000B1CFF /* Private.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Private.swift; sourceTree = ""; }; - 17CDCAD02C2E6FB0000B1CFF /* Bridging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bridging.swift; sourceTree = ""; }; - 17CDCAD32C2E8A04000B1CFF /* CGError+logString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGError+logString.swift"; sourceTree = ""; }; - 17D1AC8C2B97BB5900726180 /* MenuBarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItem.swift; sourceTree = ""; }; - 17D1AC8E2B97E71D00726180 /* AdvancedSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettingsPane.swift; sourceTree = ""; }; - 17DFF4AB2AD5FB3300B5177A /* MenuBarAppearanceSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAppearanceSettingsPane.swift; sourceTree = ""; }; - 17DFF4BF2AD8DBC500B5177A /* CodableColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableColor.swift; sourceTree = ""; }; - 17EC6B572AE0C34A0065F260 /* Comparable+clamped.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comparable+clamped.swift"; sourceTree = ""; }; - 17ECD4ED2B290F8D00E33147 /* PermissionsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsWindow.swift; sourceTree = ""; }; - 17EE695D2C26618F00F778A7 /* AnyInsettableShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyInsettableShape.swift; sourceTree = ""; }; - 17EE69602C26A80300F778A7 /* CGColor+brightness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGColor+brightness.swift"; sourceTree = ""; }; - 17EE69642C27777C00F778A7 /* MenuBarItemImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItemImageCache.swift; sourceTree = ""; }; - 17EE69672C277B4E00F778A7 /* Publisher+mapToVoid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+mapToVoid.swift"; sourceTree = ""; }; - 17F71BB12B87E37800905CBA /* RehideStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RehideStrategy.swift; sourceTree = ""; }; - 17F93D2B2C20FC440058C0AF /* Sequence+sortedByOrderInMenuBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sequence+sortedByOrderInMenuBar.swift"; sourceTree = ""; }; - 17F998312C15C91400D75EC0 /* MouseCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseCursor.swift; sourceTree = ""; }; - 17F9983B2C1655EF00D75EC0 /* LayoutBarContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutBarContainer.swift; sourceTree = ""; }; - 17F9983D2C16562300D75EC0 /* LayoutBarItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutBarItemView.swift; sourceTree = ""; }; - 17F998402C16568600D75EC0 /* CGImage+trimmingTransparentPixels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+trimmingTransparentPixels.swift"; sourceTree = ""; }; - 17F998452C16C78000D75EC0 /* Injection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Injection.swift; sourceTree = ""; }; - 17F9984A2C16CBC100D75EC0 /* LayoutBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutBar.swift; sourceTree = ""; }; - 17F9984C2C16CD8E00D75EC0 /* MenuBarItemsSettingsPane.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarItemsSettingsPane.swift; sourceTree = ""; }; - 17F9984E2C16CF8600D75EC0 /* LayoutBarPaddingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutBarPaddingView.swift; sourceTree = ""; }; - 17F998502C16D02E00D75EC0 /* LayoutBarScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LayoutBarScrollView.swift; sourceTree = ""; }; - 17F998522C16EFCD00D75EC0 /* CGImage+averageColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGImage+averageColor.swift"; sourceTree = ""; }; - 17FE5AD22BCA7A7500E4F8D9 /* ScreenStateManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenStateManager.swift; sourceTree = ""; }; - 17FE5AD42BCA9BE200E4F8D9 /* ScreenState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenState.swift; sourceTree = ""; }; - 71008DEF2AB907B00036B1F3 /* ObjectAssociation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectAssociation.swift; sourceTree = ""; }; - 7105CA202C5D2EFF004E439E /* StatusItemDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemDefaults.swift; sourceTree = ""; }; - 7105CA242C5D2F44004E439E /* StatusItemDefaultsKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItemDefaultsKey.swift; sourceTree = ""; }; - 711535F12AB9F6C1003193AD /* BindingExposable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingExposable.swift; sourceTree = ""; }; - 711568AF2C59ADBF00CDF58F /* NSScreen+frameOfNotch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+frameOfNotch.swift"; sourceTree = ""; }; - 71287C5F2C3189AC0028706E /* IceBarColorManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceBarColorManager.swift; sourceTree = ""; }; - 71287C612C31D75F0028706E /* MenuBarAverageColorInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarAverageColorInfo.swift; sourceTree = ""; }; - 71287C632C3233B30028706E /* NSScreen+hasNotch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+hasNotch.swift"; sourceTree = ""; }; - 7133ED5D2A853FCF000A7E1B /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 7133ED642A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+windowWithIdentifier.swift"; sourceTree = ""; }; - 7133ED692A85870E000A7E1B /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 7150A7AC2AA4265F0045EA68 /* KeyCombination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCombination.swift; sourceTree = ""; }; - 7150A7AE2AA426C60045EA68 /* HotkeyRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HotkeyRegistry.swift; sourceTree = ""; }; - 7150A7B02AA427F80045EA68 /* KeyCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyCode.swift; sourceTree = ""; }; - 7150A7B22AA42B640045EA68 /* Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modifiers.swift; sourceTree = ""; }; - 71553E362C378BF50083F5BE /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = ""; }; - 71604D602AAE62DA002FE96F /* BottomBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomBar.swift; sourceTree = ""; }; - 7162406E2AA0A323003EC671 /* MenuBarSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarSection.swift; sourceTree = ""; }; 7166832A2A767E6A006ABF84 /* Ice.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ice.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 7166832D2A767E6A006ABF84 /* IceApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceApp.swift; sourceTree = ""; }; - 716683312A767E6B006ABF84 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 716683362A767E6B006ABF84 /* Ice.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ice.entitlements; sourceTree = ""; }; - 7166833C2A7680CE006ABF84 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 716683452A76811C006ABF84 /* Logger+initWithCategory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Logger+initWithCategory.swift"; sourceTree = ""; }; - 716683472A76811C006ABF84 /* NSStatusItem+showMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSStatusItem+showMenu.swift"; sourceTree = ""; }; - 7166834F2A7681AF006ABF84 /* MenuBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarManager.swift; sourceTree = ""; }; - 716683512A76820A006ABF84 /* ControlItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlItem.swift; sourceTree = ""; }; - 718152C22C34E595005564AA /* NSScreen+screenWithMouse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+screenWithMouse.swift"; sourceTree = ""; }; - 71829E092C2FDCC200503604 /* NSScreen+getMenuBarHeight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSScreen+getMenuBarHeight.swift"; sourceTree = ""; }; - 71839C152A9B6A0D00250044 /* OnFrameChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnFrameChange.swift; sourceTree = ""; }; - 718E3F772C679F680029B835 /* IceBarLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IceBarLocation.swift; sourceTree = ""; }; - 71C400312AAD76A8006FDB1C /* ReadWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadWindow.swift; sourceTree = ""; }; - 71D2B02D2AAFDD5C0002B6C8 /* Bundle+versionString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+versionString.swift"; sourceTree = ""; }; - 71D36C4E2A88FE4900D89CD5 /* SettingsWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindow.swift; sourceTree = ""; }; - 71F4280E2C3B5C870025706C /* SettingsNavigationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsNavigationIdentifier.swift; sourceTree = ""; }; - 71F428102C3B5CE90025706C /* NavigationIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationIdentifier.swift; sourceTree = ""; }; - 71F428122C3B5E030025706C /* IconResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconResource.swift; sourceTree = ""; }; - 71F428142C3B5F3D0025706C /* AppNavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppNavigationState.swift; sourceTree = ""; }; - 71FEA2532A8D701B0048341A /* LocalEventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalEventMonitor.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 71BDFC6C2C978E2A00EF145F /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + Resources/Acknowledgements.rtf, + ); + target = 716683292A767E6A006ABF84 /* Ice */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 71BDFBE12C978E2A00EF145F /* Ice */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (71BDFC6C2C978E2A00EF145F /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Ice; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 716683272A767E6A006ABF84 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 170423D92B56DE78004A2549 /* Sparkle in Frameworks */, + 7127A9FF2C4886D100D99DEF /* IfritStatic in Frameworks */, 175061912B1543DD003144CD /* LaunchAtLogin in Frameworks */, 1787C4272B16890B002F50DF /* AXSwift in Frameworks */, 17F71BB52B880B4500905CBA /* CompactSlider in Frameworks */, @@ -283,382 +49,11 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 170423DB2B56E759004A2549 /* Updates */ = { - isa = PBXGroup; - children = ( - 170423DC2B56E77D004A2549 /* UpdatesManager.swift */, - ); - path = Updates; - sourceTree = ""; - }; - 1706D7582B5302A60053D1E8 /* HotkeyRecorder */ = { - isa = PBXGroup; - children = ( - 174D4E232BA1D28700D11129 /* HotkeyRecorder.swift */, - 174D4E252BA1D35D00D11129 /* HotkeyRecorderModel.swift */, - ); - path = HotkeyRecorder; - sourceTree = ""; - }; - 170749BF2B11A2BB009DDF73 /* Resources */ = { - isa = PBXGroup; - children = ( - 1726A3F82B3378B8008B09DD /* Acknowledgements.rtf */, - 1787C4352B17A9EB002F50DF /* Acknowledgements.pdf */, - ); - path = Resources; - sourceTree = ""; - }; - 170749CB2B120951009DDF73 /* EventMonitors */ = { - isa = PBXGroup; - children = ( - 172778472C1490EA00F6F687 /* EventTap.swift */, - 170749C72B12078F009DDF73 /* GlobalEventMonitor.swift */, - 71FEA2532A8D701B0048341A /* LocalEventMonitor.swift */, - 17543EB02C2C7A100052711E /* RunLoopLocalEventMonitor.swift */, - 170749C92B1207D9009DDF73 /* UniversalEventMonitor.swift */, - ); - path = EventMonitors; - sourceTree = ""; - }; - 17147FBE2B99513200C4A435 /* Events */ = { - isa = PBXGroup; - children = ( - 17CC22BB2B8CDE6F001A0582 /* EventManager.swift */, - ); - path = Events; - sourceTree = ""; - }; - 171C6FC72C0659250097A5C8 /* Task */ = { - isa = PBXGroup; - children = ( - 171C6FC82C0659340097A5C8 /* Task+timeout.swift */, - ); - path = Task; - sourceTree = ""; - }; - 172778442C1489DF00F6F687 /* Frameworks */ = { - isa = PBXGroup; - children = ( - ); - name = Frameworks; - sourceTree = ""; - }; - 1736F7762ADBBEF10073428E /* CustomGradientPicker */ = { - isa = PBXGroup; - children = ( - 170CF88F2B101D980073F982 /* ColorStop.swift */, - 1736F77F2ADBC02B0073428E /* CustomGradient.swift */, - 1736F77B2ADBBF340073428E /* CustomGradientPicker.swift */, - ); - path = CustomGradientPicker; - sourceTree = ""; - }; - 177354672B1BBAB3001CF731 /* MenuBarAppearanceSettingsPane */ = { - isa = PBXGroup; - children = ( - 171C6F972C0353CE0097A5C8 /* MenuBarAppearanceEditor.swift */, - 17DFF4AB2AD5FB3300B5177A /* MenuBarAppearanceSettingsPane.swift */, - 1773546B2B1BBBA1001CF731 /* MenuBarShapePicker.swift */, - ); - path = MenuBarAppearanceSettingsPane; - sourceTree = ""; - }; - 177354822B1F9AE1001CF731 /* NSBezierPath */ = { - isa = PBXGroup; - children = ( - 17540BD72B20C0DA00A0F965 /* NSBezierPath+drawShadow.swift */, - 177354832B1F9AF9001CF731 /* NSBezierPath+union.swift */, - 17540BDB2B23BD5700A0F965 /* NSBezierPath+intersects.swift */, - ); - path = NSBezierPath; - sourceTree = ""; - }; - 177386F62B0A656100448BBF /* ControlItem */ = { - isa = PBXGroup; - children = ( - 716683512A76820A006ABF84 /* ControlItem.swift */, - 177386F22B092A0700448BBF /* ControlItemImage.swift */, - 177386F42B0A654D00448BBF /* ControlItemImageSet.swift */, - ); - path = ControlItem; - sourceTree = ""; - }; - 1787C42A2B16ADF2002F50DF /* Permissions */ = { - isa = PBXGroup; - children = ( - 1787C42B2B16AE0B002F50DF /* Permission.swift */, - 1787C42F2B16AE78002F50DF /* PermissionsManager.swift */, - 1787C4332B16AECF002F50DF /* PermissionsView.swift */, - 17ECD4ED2B290F8D00E33147 /* PermissionsWindow.swift */, - ); - path = Permissions; - sourceTree = ""; - }; - 179267372AE98870006AFBAF /* ViewModifiers */ = { - isa = PBXGroup; - children = ( - 175812582B82ACA800D622DA /* Annotation.swift */, - 71604D602AAE62DA002FE96F /* BottomBar.swift */, - 17BE3DE12C1ACAEA008B98EF /* Erased.swift */, - 17BE3DDA2C1A63B6008B98EF /* LayoutBarStyle.swift */, - 17B380F42ADCBE090002C9C3 /* LocalEventMonitorModifier.swift */, - 71839C152A9B6A0D00250044 /* OnFrameChange.swift */, - 17B380F22ADCBC8A0002C9C3 /* OnKeyDown.swift */, - 71C400312AAD76A8006FDB1C /* ReadWindow.swift */, - 179AC2FB2C1146EF0051E7B0 /* RemoveSidebarToggle.swift */, - ); - path = ViewModifiers; - sourceTree = ""; - }; - 179AC2FD2C1147500051E7B0 /* Swizzling */ = { - isa = PBXGroup; - children = ( - 179AC2FE2C11475F0051E7B0 /* NSSplitViewItem+swizzledCanCollapse.swift */, - 17BE3DDF2C1A705E008B98EF /* IceBarPanel+hasKeyAppearance.swift */, - ); - path = Swizzling; - sourceTree = ""; - }; - 179F13BC2B91E7F700EC6B52 /* MenuBarAppearance */ = { - isa = PBXGroup; - children = ( - 179F13B92B90D34800EC6B52 /* MenuBarAppearanceConfiguration.swift */, - 175812532B80FBFA00D622DA /* MenuBarAppearanceEditorPanel.swift */, - 17B7F32A2B264C1800CDCF49 /* MenuBarAppearanceManager.swift */, - 174AA5D52B71D97100E3FE74 /* MenuBarOverlayPanel.swift */, - 177354652B1B8502001CF731 /* MenuBarShape.swift */, - 1787C43A2B187187002F50DF /* MenuBarTintKind.swift */, - ); - path = MenuBarAppearance; - sourceTree = ""; - }; - 17AB36302BA081C400F50C0F /* NSImage */ = { - isa = PBXGroup; - children = ( - 17AB36312BA081D800F50C0F /* NSImage+resized.swift */, - ); - path = NSImage; - sourceTree = ""; - }; - 17B331D12B991D180084EBB0 /* SettingsManagers */ = { - isa = PBXGroup; - children = ( - 17B331D42B991D8D0084EBB0 /* AdvancedSettingsManager.swift */, - 17B331D22B991D290084EBB0 /* GeneralSettingsManager.swift */, - 174D4E292BA230FB00D11129 /* HotkeySettingsManager.swift */, - 17B331CF2B9916E70084EBB0 /* SettingsManager.swift */, - ); - path = SettingsManagers; - sourceTree = ""; - }; - 17BE3DCE2C1A458E008B98EF /* IceBar */ = { - isa = PBXGroup; - children = ( - 17BE3DCF2C1A45B3008B98EF /* IceBar.swift */, - 71287C5F2C3189AC0028706E /* IceBarColorManager.swift */, - 718E3F772C679F680029B835 /* IceBarLocation.swift */, - ); - path = IceBar; - sourceTree = ""; - }; - 17C261E72B5AEB7A0076F129 /* Main */ = { - isa = PBXGroup; - children = ( - 7166833C2A7680CE006ABF84 /* AppDelegate.swift */, - 1725FC692AED973800A59081 /* AppState.swift */, - 7166832D2A767E6A006ABF84 /* IceApp.swift */, - ); - path = Main; - sourceTree = ""; - }; - 17C3C5F52B75A35000B9648C /* NSScreen */ = { - isa = PBXGroup; - children = ( - 17C3C5F62B75A36100B9648C /* NSScreen+displayID.swift */, - 711568AF2C59ADBF00CDF58F /* NSScreen+frameOfNotch.swift */, - 71829E092C2FDCC200503604 /* NSScreen+getMenuBarHeight.swift */, - 71287C632C3233B30028706E /* NSScreen+hasNotch.swift */, - 718152C22C34E595005564AA /* NSScreen+screenWithMouse.swift */, - ); - path = NSScreen; - sourceTree = ""; - }; - 17CDCACA2C2E6F3D000B1CFF /* Bridging */ = { - isa = PBXGroup; - children = ( - 17CDCAD02C2E6FB0000B1CFF /* Bridging.swift */, - 17CDCACB2C2E6F55000B1CFF /* Shims */, - ); - path = Bridging; - sourceTree = ""; - }; - 17CDCACB2C2E6F55000B1CFF /* Shims */ = { - isa = PBXGroup; - children = ( - 17CDCACC2C2E6F6A000B1CFF /* Deprecated.swift */, - 17CDCACE2C2E6F77000B1CFF /* Private.swift */, - ); - path = Shims; - sourceTree = ""; - }; - 17CDCAD22C2E89FD000B1CFF /* CGError */ = { - isa = PBXGroup; - children = ( - 17CDCAD32C2E8A04000B1CFF /* CGError+logString.swift */, - ); - path = CGError; - sourceTree = ""; - }; - 17EC6B522AE0C09F0065F260 /* Pickers */ = { - isa = PBXGroup; - children = ( - 17EC6B532AE0C0C50065F260 /* CustomColorPicker */, - 1736F7762ADBBEF10073428E /* CustomGradientPicker */, - ); - path = Pickers; - sourceTree = ""; - }; - 17EC6B532AE0C0C50065F260 /* CustomColorPicker */ = { - isa = PBXGroup; - children = ( - 176B23F32ADB76A1008AE86B /* CustomColorPicker.swift */, - ); - path = CustomColorPicker; - sourceTree = ""; - }; - 17EC6B562AE0C3340065F260 /* Comparable */ = { - isa = PBXGroup; - children = ( - 17EC6B572AE0C34A0065F260 /* Comparable+clamped.swift */, - ); - path = Comparable; - sourceTree = ""; - }; - 17EE695C2C26617100F778A7 /* Shapes */ = { - isa = PBXGroup; - children = ( - 17EE695D2C26618F00F778A7 /* AnyInsettableShape.swift */, - ); - path = Shapes; - sourceTree = ""; - }; - 17EE695F2C26A7F600F778A7 /* CGColor */ = { - isa = PBXGroup; - children = ( - 17EE69602C26A80300F778A7 /* CGColor+brightness.swift */, - ); - path = CGColor; - sourceTree = ""; - }; - 17EE69662C277B4300F778A7 /* Publisher */ = { - isa = PBXGroup; - children = ( - 17EE69672C277B4E00F778A7 /* Publisher+mapToVoid.swift */, - ); - path = Publisher; - sourceTree = ""; - }; - 17F93D2A2C20FC340058C0AF /* Sequence */ = { - isa = PBXGroup; - children = ( - 17F93D2B2C20FC440058C0AF /* Sequence+sortedByOrderInMenuBar.swift */, - ); - path = Sequence; - sourceTree = ""; - }; - 17F9983A2C1655D200D75EC0 /* LayoutBar */ = { - isa = PBXGroup; - children = ( - 17F9984A2C16CBC100D75EC0 /* LayoutBar.swift */, - 17F9983B2C1655EF00D75EC0 /* LayoutBarContainer.swift */, - 17F9983D2C16562300D75EC0 /* LayoutBarItemView.swift */, - 17F9984E2C16CF8600D75EC0 /* LayoutBarPaddingView.swift */, - 17F998502C16D02E00D75EC0 /* LayoutBarScrollView.swift */, - ); - path = LayoutBar; - sourceTree = ""; - }; - 17F9983F2C16567E00D75EC0 /* CGImage */ = { - isa = PBXGroup; - children = ( - 17F998522C16EFCD00D75EC0 /* CGImage+averageColor.swift */, - 17F998402C16568600D75EC0 /* CGImage+trimmingTransparentPixels.swift */, - ); - path = CGImage; - sourceTree = ""; - }; - 17FE5AD62BCA9BEC00E4F8D9 /* ScreenState */ = { - isa = PBXGroup; - children = ( - 17FE5AD22BCA7A7500E4F8D9 /* ScreenStateManager.swift */, - 17FE5AD42BCA9BE200E4F8D9 /* ScreenState.swift */, - ); - path = ScreenState; - sourceTree = ""; - }; - 7105CA232C5D2F36004E439E /* StatusItemDefaults */ = { - isa = PBXGroup; - children = ( - 7105CA202C5D2EFF004E439E /* StatusItemDefaults.swift */, - 7105CA242C5D2F44004E439E /* StatusItemDefaultsKey.swift */, - ); - path = StatusItemDefaults; - sourceTree = ""; - }; - 7133ED5F2A855ADA000A7E1B /* NSApplication */ = { - isa = PBXGroup; - children = ( - 7133ED642A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift */, - ); - path = NSApplication; - sourceTree = ""; - }; - 7133ED662A858230000A7E1B /* Settings */ = { - isa = PBXGroup; - children = ( - 7133ED692A85870E000A7E1B /* SettingsView.swift */, - 71D36C4E2A88FE4900D89CD5 /* SettingsWindow.swift */, - 17B331D12B991D180084EBB0 /* SettingsManagers */, - 7162406B2A9F1607003EC671 /* SettingsPanes */, - ); - path = Settings; - sourceTree = ""; - }; - 7150A7AB2AA4262F0045EA68 /* Hotkeys */ = { - isa = PBXGroup; - children = ( - 174D4E272BA2089100D11129 /* Hotkey.swift */, - 1737E3E42BA3C4AA009F0EFA /* HotkeyAction.swift */, - 7150A7AE2AA426C60045EA68 /* HotkeyRegistry.swift */, - 7150A7B02AA427F80045EA68 /* KeyCode.swift */, - 7150A7AC2AA4265F0045EA68 /* KeyCombination.swift */, - 7150A7B22AA42B640045EA68 /* Modifiers.swift */, - 1706D7582B5302A60053D1E8 /* HotkeyRecorder */, - ); - path = Hotkeys; - sourceTree = ""; - }; - 7162406B2A9F1607003EC671 /* SettingsPanes */ = { - isa = PBXGroup; - children = ( - 173C24882B8E80830096F7A1 /* AboutSettingsPane.swift */, - 17D1AC8E2B97E71D00726180 /* AdvancedSettingsPane.swift */, - 171C6F9B2C0356BC0097A5C8 /* GeneralSettingsPane.swift */, - 17CC22AF2B8A0CA6001A0582 /* HotkeysSettingsPane.swift */, - 17F9984C2C16CD8E00D75EC0 /* MenuBarItemsSettingsPane.swift */, - 173C248B2B8E821C0096F7A1 /* UpdatesSettingsPane.swift */, - 177354672B1BBAB3001CF731 /* MenuBarAppearanceSettingsPane */, - ); - path = SettingsPanes; - sourceTree = ""; - }; 716683212A767E6A006ABF84 = { isa = PBXGroup; children = ( - 7166832C2A767E6A006ABF84 /* Ice */, + 71BDFBE12C978E2A00EF145F /* Ice */, 7166832B2A767E6A006ABF84 /* Products */, - 172778442C1489DF00F6F687 /* Frameworks */, ); sourceTree = ""; }; @@ -670,145 +65,6 @@ name = Products; sourceTree = ""; }; - 7166832C2A767E6A006ABF84 /* Ice */ = { - isa = PBXGroup; - children = ( - 17CDCACA2C2E6F3D000B1CFF /* Bridging */, - 177386F62B0A656100448BBF /* ControlItem */, - 17147FBE2B99513200C4A435 /* Events */, - 716683412A76811C006ABF84 /* Extensions */, - 7150A7AB2AA4262F0045EA68 /* Hotkeys */, - 17BE3DCE2C1A458E008B98EF /* IceBar */, - 17F9983A2C1655D200D75EC0 /* LayoutBar */, - 17C261E72B5AEB7A0076F129 /* Main */, - 7166834E2A768190006ABF84 /* MenuBar */, - 179F13BC2B91E7F700EC6B52 /* MenuBarAppearance */, - 71F4280B2C3B5BF00025706C /* Navigation */, - 1787C42A2B16ADF2002F50DF /* Permissions */, - 7133ED662A858230000A7E1B /* Settings */, - 71D36C522A89443800D89CD5 /* UI */, - 170423DB2B56E759004A2549 /* Updates */, - 7166833E2A76811C006ABF84 /* Utilities */, - 170749BF2B11A2BB009DDF73 /* Resources */, - 716683312A767E6B006ABF84 /* Assets.xcassets */, - 716683362A767E6B006ABF84 /* Ice.entitlements */, - 17C261E22B5AC03C0076F129 /* Info.plist */, - ); - path = Ice; - sourceTree = ""; - }; - 7166833E2A76811C006ABF84 /* Utilities */ = { - isa = PBXGroup; - children = ( - 170CF88D2B0EDD780073F982 /* AnyLocalizedError.swift */, - 711535F12AB9F6C1003193AD /* BindingExposable.swift */, - 17DFF4BF2AD8DBC500B5177A /* CodableColor.swift */, - 7133ED5D2A853FCF000A7E1B /* Constants.swift */, - 17928F192AC5DF9C0016C615 /* Defaults.swift */, - 71F428122C3B5E030025706C /* IconResource.swift */, - 17F998452C16C78000D75EC0 /* Injection.swift */, - 17C303772BA62CD20079755B /* MigrationManager.swift */, - 17F998312C15C91400D75EC0 /* MouseCursor.swift */, - 71553E362C378BF50083F5BE /* Notifications.swift */, - 71008DEF2AB907B00036B1F3 /* ObjectAssociation.swift */, - 17C298B72BD42C390074DEAC /* Predicates.swift */, - 17CC22B42B8A55E7001A0582 /* WindowInfo.swift */, - 170749CB2B120951009DDF73 /* EventMonitors */, - 17FE5AD62BCA9BEC00E4F8D9 /* ScreenState */, - 7105CA232C5D2F36004E439E /* StatusItemDefaults */, - 179AC2FD2C1147500051E7B0 /* Swizzling */, - ); - path = Utilities; - sourceTree = ""; - }; - 716683412A76811C006ABF84 /* Extensions */ = { - isa = PBXGroup; - children = ( - 71D2B02A2AAFD88E0002B6C8 /* Bundle */, - 17EE695F2C26A7F600F778A7 /* CGColor */, - 17CDCAD22C2E89FD000B1CFF /* CGError */, - 17F9983F2C16567E00D75EC0 /* CGImage */, - 17EC6B562AE0C3340065F260 /* Comparable */, - 716683442A76811C006ABF84 /* Logger */, - 7133ED5F2A855ADA000A7E1B /* NSApplication */, - 177354822B1F9AE1001CF731 /* NSBezierPath */, - 17AB36302BA081C400F50C0F /* NSImage */, - 17C3C5F52B75A35000B9648C /* NSScreen */, - 716683462A76811C006ABF84 /* NSStatusItem */, - 17EE69662C277B4300F778A7 /* Publisher */, - 17F93D2A2C20FC340058C0AF /* Sequence */, - 171C6FC72C0659250097A5C8 /* Task */, - ); - path = Extensions; - sourceTree = ""; - }; - 716683442A76811C006ABF84 /* Logger */ = { - isa = PBXGroup; - children = ( - 716683452A76811C006ABF84 /* Logger+initWithCategory.swift */, - ); - path = Logger; - sourceTree = ""; - }; - 716683462A76811C006ABF84 /* NSStatusItem */ = { - isa = PBXGroup; - children = ( - 716683472A76811C006ABF84 /* NSStatusItem+showMenu.swift */, - ); - path = NSStatusItem; - sourceTree = ""; - }; - 7166834E2A768190006ABF84 /* MenuBar */ = { - isa = PBXGroup; - children = ( - 71287C612C31D75F0028706E /* MenuBarAverageColorInfo.swift */, - 17D1AC8C2B97BB5900726180 /* MenuBarItem.swift */, - 17EE69642C27777C00F778A7 /* MenuBarItemImageCache.swift */, - 1727786D2C15181300F6F687 /* MenuBarItemManager.swift */, - 7166834F2A7681AF006ABF84 /* MenuBarManager.swift */, - 7162406E2AA0A323003EC671 /* MenuBarSection.swift */, - 17F71BB12B87E37800905CBA /* RehideStrategy.swift */, - ); - path = MenuBar; - sourceTree = ""; - }; - 71D2B02A2AAFD88E0002B6C8 /* Bundle */ = { - isa = PBXGroup; - children = ( - 171C6FC52C0571510097A5C8 /* Bundle+copyrightString.swift */, - 71D2B02D2AAFDD5C0002B6C8 /* Bundle+versionString.swift */, - ); - path = Bundle; - sourceTree = ""; - }; - 71D36C522A89443800D89CD5 /* UI */ = { - isa = PBXGroup; - children = ( - 17EC6B522AE0C09F0065F260 /* Pickers */, - 17EE695C2C26617100F778A7 /* Shapes */, - 179267372AE98870006AFBAF /* ViewModifiers */, - ); - path = UI; - sourceTree = ""; - }; - 71F4280B2C3B5BF00025706C /* Navigation */ = { - isa = PBXGroup; - children = ( - 71F428142C3B5F3D0025706C /* AppNavigationState.swift */, - 71F428182C3B605B0025706C /* NavigationIdentifiers */, - ); - path = Navigation; - sourceTree = ""; - }; - 71F428182C3B605B0025706C /* NavigationIdentifiers */ = { - isa = PBXGroup; - children = ( - 71F428102C3B5CE90025706C /* NavigationIdentifier.swift */, - 71F4280E2C3B5C870025706C /* SettingsNavigationIdentifier.swift */, - ); - path = NavigationIdentifiers; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -825,12 +81,16 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + 71BDFBE12C978E2A00EF145F /* Ice */, + ); name = Ice; packageProductDependencies = ( 175061902B1543DD003144CD /* LaunchAtLogin */, 1787C4262B16890B002F50DF /* AXSwift */, 170423D82B56DE78004A2549 /* Sparkle */, 17F71BB42B880B4500905CBA /* CompactSlider */, + 7127A9FE2C4886D100D99DEF /* IfritStatic */, ); productName = Ice; productReference = 7166832A2A767E6A006ABF84 /* Ice.app */; @@ -865,6 +125,7 @@ 1787C4252B16890B002F50DF /* XCRemoteSwiftPackageReference "AXSwift" */, 170423D72B56DE78004A2549 /* XCRemoteSwiftPackageReference "Sparkle" */, 17F71BB32B880B4500905CBA /* XCRemoteSwiftPackageReference "CompactSlider" */, + 7127A9FB2C4881BC00D99DEF /* XCRemoteSwiftPackageReference "Ifrit" */, ); productRefGroup = 7166832B2A767E6A006ABF84 /* Products */; projectDirPath = ""; @@ -880,8 +141,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 716683322A767E6B006ABF84 /* Assets.xcassets in Resources */, - 1787C4362B17A9EB002F50DF /* Acknowledgements.pdf in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -914,128 +173,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 17928F1A2AC5DF9C0016C615 /* Defaults.swift in Sources */, - 179F13BA2B90D34800EC6B52 /* MenuBarAppearanceConfiguration.swift in Sources */, - 171C6FC62C0571510097A5C8 /* Bundle+copyrightString.swift in Sources */, - 71FEA2542A8D701B0048341A /* LocalEventMonitor.swift in Sources */, - 7150A7AF2AA426C60045EA68 /* HotkeyRegistry.swift in Sources */, - 7162406F2AA0A323003EC671 /* MenuBarSection.swift in Sources */, - 177386F52B0A654D00448BBF /* ControlItemImageSet.swift in Sources */, - 17CDCACF2C2E6F77000B1CFF /* Private.swift in Sources */, - 17BE3DE02C1A705E008B98EF /* IceBarPanel+hasKeyAppearance.swift in Sources */, - 173C24892B8E80830096F7A1 /* AboutSettingsPane.swift in Sources */, - 177386F32B092A0700448BBF /* ControlItemImage.swift in Sources */, - 17EC6B582AE0C34A0065F260 /* Comparable+clamped.swift in Sources */, - 1787C43B2B187187002F50DF /* MenuBarTintKind.swift in Sources */, - 71287C642C3233B30028706E /* NSScreen+hasNotch.swift in Sources */, - 17F93D2C2C20FC440058C0AF /* Sequence+sortedByOrderInMenuBar.swift in Sources */, - 1725FC6A2AED973800A59081 /* AppState.swift in Sources */, - 1727786E2C15181300F6F687 /* MenuBarItemManager.swift in Sources */, - 170749CA2B1207D9009DDF73 /* UniversalEventMonitor.swift in Sources */, - 17F9983E2C16562300D75EC0 /* LayoutBarItemView.swift in Sources */, - 17F998322C15C91400D75EC0 /* MouseCursor.swift in Sources */, - 171C6FC92C0659340097A5C8 /* Task+timeout.swift in Sources */, - 71F428132C3B5E030025706C /* IconResource.swift in Sources */, - 17F71BB22B87E37800905CBA /* RehideStrategy.swift in Sources */, - 17F9984F2C16CF8600D75EC0 /* LayoutBarPaddingView.swift in Sources */, - 17B380F32ADCBC8A0002C9C3 /* OnKeyDown.swift in Sources */, - 1736F77C2ADBBF340073428E /* CustomGradientPicker.swift in Sources */, - 17543EB12C2C7A100052711E /* RunLoopLocalEventMonitor.swift in Sources */, - 17BE3DD02C1A45B3008B98EF /* IceBar.swift in Sources */, - 17C303782BA62CD20079755B /* MigrationManager.swift in Sources */, - 71F4280F2C3B5C870025706C /* SettingsNavigationIdentifier.swift in Sources */, - 17CDCACD2C2E6F6A000B1CFF /* Deprecated.swift in Sources */, - 17CC22B52B8A55E7001A0582 /* WindowInfo.swift in Sources */, - 17CC22B02B8A0CA6001A0582 /* HotkeysSettingsPane.swift in Sources */, - 17F998462C16C78000D75EC0 /* Injection.swift in Sources */, - 716683522A76820A006ABF84 /* ControlItem.swift in Sources */, - 7150A7AD2AA4265F0045EA68 /* KeyCombination.swift in Sources */, - 71839C162A9B6A0D00250044 /* OnFrameChange.swift in Sources */, - 17C298B82BD42C390074DEAC /* Predicates.swift in Sources */, - 17F998412C16568600D75EC0 /* CGImage+trimmingTransparentPixels.swift in Sources */, - 170749C82B12078F009DDF73 /* GlobalEventMonitor.swift in Sources */, - 17ECD4EE2B290F8D00E33147 /* PermissionsWindow.swift in Sources */, - 17FE5AD32BCA7A7500E4F8D9 /* ScreenStateManager.swift in Sources */, - 174D4E242BA1D28700D11129 /* HotkeyRecorder.swift in Sources */, - 71287C622C31D75F0028706E /* MenuBarAverageColorInfo.swift in Sources */, - 71604D612AAE62DA002FE96F /* BottomBar.swift in Sources */, - 174D4E262BA1D35D00D11129 /* HotkeyRecorderModel.swift in Sources */, - 1787C42C2B16AE0B002F50DF /* Permission.swift in Sources */, - 1787C4342B16AECF002F50DF /* PermissionsView.swift in Sources */, - 17F9983C2C1655EF00D75EC0 /* LayoutBarContainer.swift in Sources */, - 716683502A7681AF006ABF84 /* MenuBarManager.swift in Sources */, - 176B23F42ADB76A1008AE86B /* CustomColorPicker.swift in Sources */, - 17CDCAD12C2E6FB0000B1CFF /* Bridging.swift in Sources */, - 17540BD82B20C0DA00A0F965 /* NSBezierPath+drawShadow.swift in Sources */, - 177354842B1F9AF9001CF731 /* NSBezierPath+union.swift in Sources */, - 172778482C1490EA00F6F687 /* EventTap.swift in Sources */, - 17540BDC2B23BD5700A0F965 /* NSBezierPath+intersects.swift in Sources */, - 17F998512C16D02E00D75EC0 /* LayoutBarScrollView.swift in Sources */, - 179AC2FC2C1146EF0051E7B0 /* RemoveSidebarToggle.swift in Sources */, - 718E3F782C679F680029B835 /* IceBarLocation.swift in Sources */, - 17B7F32B2B264C1800CDCF49 /* MenuBarAppearanceManager.swift in Sources */, - 17F9984D2C16CD8E00D75EC0 /* MenuBarItemsSettingsPane.swift in Sources */, - 71F428152C3B5F3D0025706C /* AppNavigationState.swift in Sources */, - 17D1AC8D2B97BB5900726180 /* MenuBarItem.swift in Sources */, - 7105CA212C5D2EFF004E439E /* StatusItemDefaults.swift in Sources */, - 71553E372C378BF50083F5BE /* Notifications.swift in Sources */, - 173C248C2B8E821C0096F7A1 /* UpdatesSettingsPane.swift in Sources */, - 171C6F9C2C0356BC0097A5C8 /* GeneralSettingsPane.swift in Sources */, - 7150A7B12AA427F80045EA68 /* KeyCode.swift in Sources */, - 71829E0A2C2FDCC200503604 /* NSScreen+getMenuBarHeight.swift in Sources */, - 17B331D02B9916E70084EBB0 /* SettingsManager.swift in Sources */, - 17B331D32B991D290084EBB0 /* GeneralSettingsManager.swift in Sources */, - 1736F7802ADBC02B0073428E /* CustomGradient.swift in Sources */, - 7166834C2A76811C006ABF84 /* NSStatusItem+showMenu.swift in Sources */, - 711568B02C59ADBF00CDF58F /* NSScreen+frameOfNotch.swift in Sources */, - 17BE3DDB2C1A63B6008B98EF /* LayoutBarStyle.swift in Sources */, - 711535F22AB9F6C1003193AD /* BindingExposable.swift in Sources */, - 7166833D2A7680CE006ABF84 /* AppDelegate.swift in Sources */, - 17EE695E2C26618F00F778A7 /* AnyInsettableShape.swift in Sources */, - 7133ED6A2A85870E000A7E1B /* SettingsView.swift in Sources */, - 7105CA252C5D2F44004E439E /* StatusItemDefaultsKey.swift in Sources */, - 7133ED652A85811C000A7E1B /* NSApplication+windowWithIdentifier.swift in Sources */, - 7150A7B32AA42B640045EA68 /* Modifiers.swift in Sources */, - 179AC2FF2C11475F0051E7B0 /* NSSplitViewItem+swizzledCanCollapse.swift in Sources */, - 71C400322AAD76A8006FDB1C /* ReadWindow.swift in Sources */, - 71D2B02E2AAFDD5C0002B6C8 /* Bundle+versionString.swift in Sources */, - 17EE69612C26A80300F778A7 /* CGColor+brightness.swift in Sources */, - 174D4E282BA2089100D11129 /* Hotkey.swift in Sources */, - 17BE3DE22C1ACAEA008B98EF /* Erased.swift in Sources */, - 7166834B2A76811C006ABF84 /* Logger+initWithCategory.swift in Sources */, - 17B380F52ADCBE090002C9C3 /* LocalEventMonitorModifier.swift in Sources */, - 17B331D52B991D8D0084EBB0 /* AdvancedSettingsManager.swift in Sources */, - 170CF8902B101D980073F982 /* ColorStop.swift in Sources */, - 1773546C2B1BBBA1001CF731 /* MenuBarShapePicker.swift in Sources */, - 17D1AC8F2B97E71D00726180 /* AdvancedSettingsPane.swift in Sources */, - 718152C32C34E595005564AA /* NSScreen+screenWithMouse.swift in Sources */, - 17F998532C16EFCD00D75EC0 /* CGImage+averageColor.swift in Sources */, - 71287C602C3189AC0028706E /* IceBarColorManager.swift in Sources */, - 71F428112C3B5CE90025706C /* NavigationIdentifier.swift in Sources */, - 17DFF4C02AD8DBC500B5177A /* CodableColor.swift in Sources */, - 17CC22BC2B8CDE6F001A0582 /* EventManager.swift in Sources */, - 1737E3E52BA3C4AA009F0EFA /* HotkeyAction.swift in Sources */, - 17EE69682C277B4E00F778A7 /* Publisher+mapToVoid.swift in Sources */, - 17EE69652C27777C00F778A7 /* MenuBarItemImageCache.swift in Sources */, - 174AA5D62B71D97100E3FE74 /* MenuBarOverlayPanel.swift in Sources */, - 71008DF02AB907B00036B1F3 /* ObjectAssociation.swift in Sources */, - 171C6F982C0353CE0097A5C8 /* MenuBarAppearanceEditor.swift in Sources */, - 174D4E2A2BA230FB00D11129 /* HotkeySettingsManager.swift in Sources */, - 17DFF4AC2AD5FB3300B5177A /* MenuBarAppearanceSettingsPane.swift in Sources */, - 17FE5AD52BCA9BE200E4F8D9 /* ScreenState.swift in Sources */, - 170423DD2B56E77D004A2549 /* UpdatesManager.swift in Sources */, - 177354662B1B8502001CF731 /* MenuBarShape.swift in Sources */, - 71D36C4F2A88FE4900D89CD5 /* SettingsWindow.swift in Sources */, - 175812592B82ACA900D622DA /* Annotation.swift in Sources */, - 17AB36322BA081D800F50C0F /* NSImage+resized.swift in Sources */, - 1787C4302B16AE78002F50DF /* PermissionsManager.swift in Sources */, - 170CF88E2B0EDD780073F982 /* AnyLocalizedError.swift in Sources */, - 175812542B80FBFA00D622DA /* MenuBarAppearanceEditorPanel.swift in Sources */, - 17CDCAD42C2E8A04000B1CFF /* CGError+logString.swift in Sources */, - 7166832E2A767E6A006ABF84 /* IceApp.swift in Sources */, - 17C3C5F72B75A36100B9648C /* NSScreen+displayID.swift in Sources */, - 7133ED5E2A853FCF000A7E1B /* Constants.swift in Sources */, - 17F9984B2C16CBC100D75EC0 /* LayoutBar.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1170,7 +307,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 1103; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = K2ATHQPJDP; @@ -1186,7 +323,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.10.5; + MARKETING_VERSION = 0.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.jordanbaird.Ice; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1203,7 +340,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 57; + CURRENT_PROJECT_VERSION = 1103; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = K2ATHQPJDP; @@ -1219,7 +356,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 0.10.5; + MARKETING_VERSION = 0.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.jordanbaird.Ice; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1283,6 +420,14 @@ minimumVersion = 1.1.5; }; }; + 7127A9FB2C4881BC00D99DEF /* XCRemoteSwiftPackageReference "Ifrit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ukushu/Ifrit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1306,6 +451,11 @@ package = 17F71BB32B880B4500905CBA /* XCRemoteSwiftPackageReference "CompactSlider" */; productName = CompactSlider; }; + 7127A9FE2C4886D100D99DEF /* IfritStatic */ = { + isa = XCSwiftPackageProductDependency; + package = 7127A9FB2C4881BC00D99DEF /* XCRemoteSwiftPackageReference "Ifrit" */; + productName = IfritStatic; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 716683222A767E6A006ABF84 /* Project object */; diff --git a/Ice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Ice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3ccb3901..116a3e50 100644 --- a/Ice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Ice.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "041f42d8ff3842bfd2a979ca1640e0292a5910cb327bf60beb5977a50e12edf5", + "originHash" : "24a4ad9c87c9d787a96ab567d6386b59cd16d86f1ddaee8967a10a304dca7993", "pins" : [ { "identity" : "axswift", @@ -19,6 +19,15 @@ "version" : "1.1.6" } }, + { + "identity" : "ifrit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ukushu/Ifrit", + "state" : { + "revision" : "6c8a4bc89881e641c1ca8e559d7f41ad030d8561", + "version" : "1.1.1" + } + }, { "identity" : "launchatlogin-modern", "kind" : "remoteSourceControl", diff --git a/Ice/Hotkeys/HotkeyAction.swift b/Ice/Hotkeys/HotkeyAction.swift index d4584b4d..dfab592a 100644 --- a/Ice/Hotkeys/HotkeyAction.swift +++ b/Ice/Hotkeys/HotkeyAction.swift @@ -8,9 +8,10 @@ enum HotkeyAction: String, Codable, CaseIterable { case toggleAlwaysHiddenSection = "ToggleAlwaysHiddenSection" case toggleApplicationMenus = "ToggleApplicationMenus" case showSectionDividers = "ShowSectionDividers" + case searchMenuBarItems = "SearchMenuBarItems" @MainActor - func perform(appState: AppState) { + func perform(appState: AppState) async { switch self { case .toggleHiddenSection: guard let section = appState.menuBarManager.section(withName: .hidden) else { @@ -34,6 +35,8 @@ enum HotkeyAction: String, Codable, CaseIterable { appState.menuBarManager.toggleApplicationMenus() case .showSectionDividers: appState.settingsManager.advancedSettingsManager.showSectionDividers.toggle() + case .searchMenuBarItems: + await appState.menuBarManager.searchPanel.toggle() } } } diff --git a/Ice/Hotkeys/HotkeyRecorder/HotkeyRecorder.swift b/Ice/Hotkeys/HotkeyRecorder/HotkeyRecorder.swift index 3f37c02b..d382b364 100644 --- a/Ice/Hotkeys/HotkeyRecorder/HotkeyRecorder.swift +++ b/Ice/Hotkeys/HotkeyRecorder/HotkeyRecorder.swift @@ -16,14 +16,20 @@ struct HotkeyRecorder: View { } var body: some View { - LabeledContent { + IceLabeledContent { HStack(spacing: 1) { leadingSegment trailingSegment } .frame(width: 130, height: 22) + .alignmentGuide(.firstTextBaseline) { dimension in + dimension[VerticalAlignment.center] + } } label: { label + .alignmentGuide(.firstTextBaseline) { dimension in + dimension[VerticalAlignment.center] + } } .alert( "Hotkey is reserved by macOS", @@ -128,9 +134,8 @@ private struct HotkeyRecorderSegmentButtonStyle: PrimitiveButtonStyle { } func makeBody(configuration: Configuration) -> some View { - UnevenRoundedRectangle(cornerRadii: radii) - .foregroundStyle(Color.primary) // explicitly specify `Color.primary` - .opacity(isHighlighted || isPressed ? 0.2 : 0.1) + UnevenRoundedRectangle(cornerRadii: radii, style: .circular) + .fill(isHighlighted || isPressed ? .tertiary : .quaternary) .overlay { configuration.label .lineLimit(1) diff --git a/Ice/Main/AppDelegate.swift b/Ice/Main/AppDelegate.swift index 09933f53..fd897475 100644 --- a/Ice/Main/AppDelegate.swift +++ b/Ice/Main/AppDelegate.swift @@ -7,7 +7,7 @@ import OSLog import SwiftUI @MainActor -class AppDelegate: NSObject, NSApplicationDelegate { +final class AppDelegate: NSObject, NSApplicationDelegate { private weak var appState: AppState? // MARK: NSApplicationDelegate Methods @@ -34,18 +34,6 @@ class AppDelegate: NSObject, NSApplicationDelegate { return } - // Assign and close the various windows. - let windowAssignments: KeyValuePairs = [ - Constants.settingsWindowID: appState.assignSettingsWindow, - Constants.permissionsWindowID: appState.assignPermissionsWindow, - ] - for (identifier, assign) in windowAssignments { - if let window = NSApp.window(withIdentifier: identifier) { - assign(window) - window.close() - } - } - // Hide the main menu to make more space in the menu bar. if let mainMenu = NSApp.mainMenu { for item in mainMenu.items { @@ -53,6 +41,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + // On macOS 15, the windows handle their own closure. If on macOS 14, + // close them here. + // + // NOTE: The windows might not close when running from Xcode, but it + // does work when running standalone. + if #unavailable(macOS 15.0) { + appState.settingsWindow?.close() + appState.permissionsWindow?.close() + } + if !appState.isPreview { // If we have the required permissions, set up the shared app state. // Otherwise, open the permissions window. @@ -95,7 +93,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { let appState, let settingsWindow = appState.settingsWindow else { - Logger.appDelegate.warning("Failed to open settings window") + Logger.appDelegate.error("Failed to open settings window") return } // Small delay makes this more reliable. diff --git a/Ice/Main/AppState.swift b/Ice/Main/AppState.swift index 4d9c7b5e..850ee0fc 100644 --- a/Ice/Main/AppState.swift +++ b/Ice/Main/AppState.swift @@ -13,8 +13,6 @@ final class AppState: ObservableObject { /// A Boolean value that indicates whether the active space is fullscreen. @Published private(set) var isActiveSpaceFullscreen = Bridging.isSpaceFullscreen(Bridging.activeSpaceID) - private var cancellables = Set() - /// Manager for events received by the app. private(set) lazy var eventManager = EventManager(appState: self) @@ -33,8 +31,8 @@ final class AppState: ObservableObject { /// Global cache for menu bar item images. private(set) lazy var imageCache = MenuBarItemImageCache(appState: self) - /// The app's hotkey registry. - nonisolated let hotkeyRegistry = HotkeyRegistry() + /// Manager for menu bar item spacing. + let spacingManager = MenuBarItemSpacingManager() /// Manager for app updates. let updatesManager = UpdatesManager() @@ -42,26 +40,17 @@ final class AppState: ObservableObject { /// Model for app-wide navigation. let navigationState = AppNavigationState() + /// The app's hotkey registry. + nonisolated let hotkeyRegistry = HotkeyRegistry() + /// The app's delegate. private(set) weak var appDelegate: AppDelegate? - /// The window that contains the settings interface. - private(set) weak var settingsWindow: NSWindow? - - /// The window that contains the permissions interface. - private(set) weak var permissionsWindow: NSWindow? - /// A Boolean value that indicates whether the "ShowOnHover" feature is prevented. private(set) var isShowOnHoverPrevented = false - /// A Boolean value that indicates whether the application can set the - /// cursor in the background. - /// - /// The default value of this property is `false`. - var setsCursorInBackground: Bool { - get { Bridging.getConnectionProperty(forKey: "SetsCursorInBackground") as? Bool ?? false } - set { Bridging.setConnectionProperty(newValue, forKey: "SetsCursorInBackground") } - } + /// Storage for internal observers. + private var cancellables = Set() /// A Boolean value that indicates whether the app is running as a SwiftUI preview. let isPreview: Bool = { @@ -74,8 +63,21 @@ final class AppState: ObservableObject { #endif }() - init() { - MigrationManager(appState: self).migrateAll() + /// The window that contains the settings interface. + var settingsWindow: NSWindow? { + NSApp.window(withIdentifier: Constants.settingsWindowID) + } + + /// The window that contains the permissions interface. + var permissionsWindow: NSWindow? { + NSApp.window(withIdentifier: Constants.permissionsWindowID) + } + + /// A Boolean value that indicates whether the application can set the cursor + /// in the background. + var setsCursorInBackground: Bool { + get { Bridging.getConnectionProperty(forKey: "SetsCursorInBackground") as? Bool ?? false } + set { Bridging.setConnectionProperty(newValue, forKey: "SetsCursorInBackground") } } private func configureCancellables() { @@ -85,12 +87,12 @@ final class AppState: ObservableObject { NSWorkspace.shared.notificationCenter .publisher(for: NSWorkspace.activeSpaceDidChangeNotification) .mapToVoid(), - // frontmost application change can indicate a space change from one display to - // another, which gets ignored by `NSWorkspace.activeSpaceDidChangeNotification` + // Frontmost application change can indicate a space change from one display to + // another, which gets ignored by NSWorkspace.activeSpaceDidChangeNotification. NSWorkspace.shared .publisher(for: \.frontmostApplication) .mapToVoid(), - // clicking into a fullscreen space from another space is also ignored + // Clicking into a fullscreen space from another space is also ignored. UniversalEventMonitor .publisher(for: .leftMouseDown) .delay(for: 0.1, scheduler: DispatchQueue.main) @@ -124,6 +126,8 @@ final class AppState: ObservableObject { navigationState.isSettingsPresented = isVisible } .store(in: &c) + } else { + Logger.appState.warning("No settings window!") } Publishers.Merge( @@ -173,7 +177,6 @@ final class AppState: ObservableObject { settingsManager.performSetup() itemManager.performSetup() imageCache.performSetup() - permissionsWindow?.close() } /// Assigns the app delegate to the app state. @@ -185,28 +188,10 @@ final class AppState: ObservableObject { self.appDelegate = appDelegate } - /// Assigns the settings window to the app state. - func assignSettingsWindow(_ settingsWindow: NSWindow) { - guard self.settingsWindow == nil else { - Logger.appState.warning("Multiple attempts made to assign settings window") - return - } - self.settingsWindow = settingsWindow - } - - /// Assigns the permissions window to the app state. - func assignPermissionsWindow(_ permissionsWindow: NSWindow) { - guard self.permissionsWindow == nil else { - Logger.appState.warning("Multiple attempts made to assign permissions window") - return - } - self.permissionsWindow = permissionsWindow - } - /// Activates the app and sets its activation policy to the given value. func activate(withPolicy policy: NSApplication.ActivationPolicy) { - // store whether the app has previously activated inside an internal - // context to keep it isolated + // Store whether the app has previously activated inside an internal + // context to keep it isolated. enum Context { static let hasActivated = ObjectAssociation() } @@ -225,7 +210,7 @@ final class AppState: ObservableObject { } else { Context.hasActivated[self] = true Logger.appState.debug("First time activating app, so going through Dock") - // hack to make sure the app properly activates for the first time + // Hack to make sure the app properly activates for the first time. NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.activate() DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { activate() diff --git a/Ice/Main/IceApp.swift b/Ice/Main/IceApp.swift index 286bd2b0..24ea8b36 100644 --- a/Ice/Main/IceApp.swift +++ b/Ice/Main/IceApp.swift @@ -9,25 +9,16 @@ import SwiftUI struct IceApp: App { @NSApplicationDelegateAdaptor var appDelegate: AppDelegate @ObservedObject var appState = AppState() - @Environment(\.openWindow) private var openWindow init() { NSSplitViewItem.swizzle() IceBarPanel.swizzle() - // Occurs before AppDelegate.applicationWillFinishLaunching(_:). + MigrationManager(appState: appState).migrateAll() appDelegate.assignAppState(appState) } var body: some Scene { - SettingsWindow(appState: appState, onAppear: { - // Open the permissions window no matter what, so that we can - // reference it. We'll close it in AppDelegate if permissions - // have already been granted. - openWindow(id: Constants.permissionsWindowID) - }) - PermissionsWindow(appState: appState, onContinue: { - appState.performSetup() - openWindow(id: Constants.settingsWindowID) - }) + SettingsWindow(appState: appState) + PermissionsWindow(appState: appState) } } diff --git a/Ice/Navigation/AppNavigationState.swift b/Ice/Main/Navigation/AppNavigationState.swift similarity index 89% rename from Ice/Navigation/AppNavigationState.swift rename to Ice/Main/Navigation/AppNavigationState.swift index 9833195e..e06a6f42 100644 --- a/Ice/Navigation/AppNavigationState.swift +++ b/Ice/Main/Navigation/AppNavigationState.swift @@ -11,5 +11,6 @@ final class AppNavigationState: ObservableObject { @Published var isAppFrontmost = false @Published var isSettingsPresented = false @Published var isIceBarPresented = false + @Published var isSearchPresented = false @Published var settingsNavigationIdentifier: SettingsNavigationIdentifier = .general } diff --git a/Ice/Navigation/NavigationIdentifiers/NavigationIdentifier.swift b/Ice/Main/Navigation/NavigationIdentifiers/NavigationIdentifier.swift similarity index 100% rename from Ice/Navigation/NavigationIdentifiers/NavigationIdentifier.swift rename to Ice/Main/Navigation/NavigationIdentifiers/NavigationIdentifier.swift diff --git a/Ice/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift b/Ice/Main/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift similarity index 89% rename from Ice/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift rename to Ice/Main/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift index 027da58d..31530d7a 100644 --- a/Ice/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift +++ b/Ice/Main/Navigation/NavigationIdentifiers/SettingsNavigationIdentifier.swift @@ -6,7 +6,7 @@ /// An identifier used for navigation in the settings interface. enum SettingsNavigationIdentifier: String, NavigationIdentifier { case general = "General" - case menuBarItems = "Menu Bar Items" + case menuBarLayout = "Menu Bar Layout" case menuBarAppearance = "Menu Bar Appearance" case hotkeys = "Hotkeys" case advanced = "Advanced" diff --git a/Ice/MenuBarAppearance/MenuBarAppearanceConfiguration.swift b/Ice/MenuBar/Appearance/MenuBarAppearanceConfiguration.swift similarity index 99% rename from Ice/MenuBarAppearance/MenuBarAppearanceConfiguration.swift rename to Ice/MenuBar/Appearance/MenuBarAppearanceConfiguration.swift index dc6abb9b..9169934c 100644 --- a/Ice/MenuBarAppearance/MenuBarAppearanceConfiguration.swift +++ b/Ice/MenuBar/Appearance/MenuBarAppearanceConfiguration.swift @@ -98,7 +98,7 @@ extension MenuBarAppearanceConfiguration { static let defaultConfiguration = MenuBarAppearanceConfiguration( hasShadow: false, hasBorder: false, - isInset: false, + isInset: true, borderColor: .black, borderWidth: 1, shapeKind: .none, diff --git a/Ice/MenuBarAppearance/MenuBarAppearanceEditorPanel.swift b/Ice/MenuBar/Appearance/MenuBarAppearanceEditorPanel.swift similarity index 100% rename from Ice/MenuBarAppearance/MenuBarAppearanceEditorPanel.swift rename to Ice/MenuBar/Appearance/MenuBarAppearanceEditorPanel.swift diff --git a/Ice/MenuBarAppearance/MenuBarAppearanceManager.swift b/Ice/MenuBar/Appearance/MenuBarAppearanceManager.swift similarity index 100% rename from Ice/MenuBarAppearance/MenuBarAppearanceManager.swift rename to Ice/MenuBar/Appearance/MenuBarAppearanceManager.swift diff --git a/Ice/MenuBarAppearance/MenuBarOverlayPanel.swift b/Ice/MenuBar/Appearance/MenuBarOverlayPanel.swift similarity index 100% rename from Ice/MenuBarAppearance/MenuBarOverlayPanel.swift rename to Ice/MenuBar/Appearance/MenuBarOverlayPanel.swift diff --git a/Ice/MenuBarAppearance/MenuBarShape.swift b/Ice/MenuBar/Appearance/MenuBarShape.swift similarity index 100% rename from Ice/MenuBarAppearance/MenuBarShape.swift rename to Ice/MenuBar/Appearance/MenuBarShape.swift diff --git a/Ice/MenuBarAppearance/MenuBarTintKind.swift b/Ice/MenuBar/Appearance/MenuBarTintKind.swift similarity index 100% rename from Ice/MenuBarAppearance/MenuBarTintKind.swift rename to Ice/MenuBar/Appearance/MenuBarTintKind.swift diff --git a/Ice/MenuBar/MenuBarItemImageCache.swift b/Ice/MenuBar/MenuBarItemImageCache.swift index fce2b00e..305238b2 100644 --- a/Ice/MenuBar/MenuBarItemImageCache.swift +++ b/Ice/MenuBar/MenuBarItemImageCache.swift @@ -206,7 +206,7 @@ class MenuBarItemImageCache: ObservableObject { return } - if !appState.navigationState.isIceBarPresented { + if !appState.navigationState.isIceBarPresented && !appState.navigationState.isSearchPresented { guard appState.navigationState.isAppFrontmost else { Logger.imageCache.debug("Skipping image cache as Ice Bar not visible, app not frontmost") return @@ -215,8 +215,8 @@ class MenuBarItemImageCache: ObservableObject { Logger.imageCache.debug("Skipping image cache as Ice Bar not visible, Settings not visible") return } - guard case .menuBarItems = appState.navigationState.settingsNavigationIdentifier else { - Logger.imageCache.debug("Skipping image cache as Ice Bar not visible, Settings visible but not on Menu Bar Items pane") + guard case .menuBarLayout = appState.navigationState.settingsNavigationIdentifier else { + Logger.imageCache.debug("Skipping image cache as Ice Bar not visible, Settings visible but not on Menu Bar Layout pane") return } } @@ -229,7 +229,7 @@ class MenuBarItemImageCache: ObservableObject { } var sectionsNeedingDisplay = [MenuBarSection.Name]() - if appState.navigationState.isSettingsPresented { + if appState.navigationState.isSettingsPresented || appState.navigationState.isSearchPresented { sectionsNeedingDisplay = MenuBarSection.Name.allCases } else if appState.navigationState.isIceBarPresented, diff --git a/Ice/MenuBar/MenuBarItemManager.swift b/Ice/MenuBar/MenuBarItemManager.swift index 75b989a7..0938fd38 100644 --- a/Ice/MenuBar/MenuBarItemManager.swift +++ b/Ice/MenuBar/MenuBarItemManager.swift @@ -17,6 +17,18 @@ class MenuBarItemManager: ObservableObject { private var items = [MenuBarSection.Name: [MenuBarItem]]() + var allItems: [MenuBarItem] { + MenuBarSection.Name.allCases.reduce(into: []) { result, section in + result.append(contentsOf: allItems(for: section)) + } + } + + var managedItems: [MenuBarItem] { + MenuBarSection.Name.allCases.reduce(into: []) { result, section in + result.append(contentsOf: managedItems(for: section)) + } + } + /// Appends the given item to the given section. mutating func appendItem(_ item: MenuBarItem, to section: MenuBarSection.Name) { items[section, default: []].append(item) @@ -1079,8 +1091,6 @@ extension MenuBarItemManager { return } - let rehideInterval: TimeInterval = 20 - guard let appState, let screen = NSScreen.main, @@ -1160,8 +1170,7 @@ extension MenuBarItemManager { let context = TempShownItemContext(info: item.info, returnDestination: destination, shownInterfaceWindow: shownInterfaceWindow) tempShownItemContexts.append(context) - - runTempShownItemTimer(for: rehideInterval) + runTempShownItemTimer(for: appState.settingsManager.advancedSettingsManager.tempShowInterval) } } diff --git a/Ice/MenuBar/MenuBarManager.swift b/Ice/MenuBar/MenuBarManager.swift index 61e2c4c8..55a45300 100644 --- a/Ice/MenuBar/MenuBarManager.swift +++ b/Ice/MenuBar/MenuBarManager.swift @@ -29,6 +29,8 @@ final class MenuBarManager: ObservableObject { let appearanceManager: MenuBarAppearanceManager let iceBarPanel: IceBarPanel + let searchPanel: MenuBarSearchPanel + private let encoder = JSONEncoder() private let decoder = JSONDecoder() @@ -53,6 +55,7 @@ final class MenuBarManager: ObservableObject { init(appState: AppState) { self.appearanceManager = MenuBarAppearanceManager(appState: appState) self.iceBarPanel = IceBarPanel(appState: appState) + self.searchPanel = MenuBarSearchPanel(appState: appState) self.appState = appState } diff --git a/Ice/MenuBar/Search/MenuBarSearchPanel.swift b/Ice/MenuBar/Search/MenuBarSearchPanel.swift new file mode 100644 index 00000000..d7d178f9 --- /dev/null +++ b/Ice/MenuBar/Search/MenuBarSearchPanel.swift @@ -0,0 +1,438 @@ +// +// MenuBarSearchPanel.swift +// Ice +// + +import Combine +import Ifrit +import SwiftUI + +class MenuBarSearchPanel: NSPanel { + private weak var appState: AppState? + + private var mouseDownMonitor: UniversalEventMonitor? + + private var keyDownMonitor: UniversalEventMonitor? + + private var cancellables = Set() + + override var canBecomeKey: Bool { true } + + init(appState: AppState) { + super.init( + contentRect: .zero, + styleMask: [.titled, .fullSizeContentView, .nonactivatingPanel, .hudWindow], + backing: .buffered, + defer: false + ) + self.appState = appState + self.titlebarAppearsTransparent = true + self.isMovableByWindowBackground = false + self.animationBehavior = .none + self.level = .floating + configureCancellables() + } + + private func configureCancellables() { + var c = Set() + + NSApp.publisher(for: \.effectiveAppearance) + .sink { [weak self] effectiveAppearance in + self?.appearance = effectiveAppearance + } + .store(in: &c) + + // close the panel when the active space changes, or when the + // screen parameters change + Publishers.Merge( + NSWorkspace.shared.notificationCenter.publisher(for: NSWorkspace.activeSpaceDidChangeNotification), + NotificationCenter.default.publisher(for: NSApplication.didChangeScreenParametersNotification) + ) + .sink { [weak self] _ in + self?.close() + } + .store(in: &c) + + cancellables = c + } + + func show(on screen: NSScreen) async { + guard let appState else { + return + } + + // Set the desired frame size for the panel before positioning + let panelSize = CGSize(width: 600, height: 400) + setFrame(NSRect(origin: .zero, size: panelSize), display: false) + + // Important that we set the navigation before updating the cache + appState.navigationState.isSearchPresented = true + + await appState.imageCache.updateCache() + + contentView = MenuBarSearchHostingView(appState: appState, closePanel: { [weak self] in + self?.close() + }) + + mouseDownMonitor = UniversalEventMonitor(mask: [.leftMouseDown, .rightMouseDown, .otherMouseDown]) { [weak self, weak appState] event in + guard + let self, + let appState, + event.window !== self + else { + return event + } + if let lastItemMoveStartDate = appState.itemManager.lastItemMoveStartDate { + guard Date.now.timeIntervalSince(lastItemMoveStartDate) > 1 else { + return event + } + } + close() + return event + } + keyDownMonitor = UniversalEventMonitor(mask: .keyDown) { [weak self] event in + if KeyCode(rawValue: Int(event.keyCode)) == .escape { + self?.close() + return nil + } + return event + } + + mouseDownMonitor?.start() + keyDownMonitor?.start() + + // Calculate the top-left position + let topLeft = CGPoint( + x: screen.frame.midX - frame.width / 2, + y: screen.frame.midY + (frame.height / 2) + (screen.frame.height / 8) + ) + + cascadeTopLeft(from: topLeft) + makeKeyAndOrderFront(nil) + } + + func toggle() async { + if isVisible { + close() + } else if let screen = NSScreen.screenWithMouse ?? NSScreen.main { + await show(on: screen) + } + } + + override func close() { + super.close() + contentView = nil + mouseDownMonitor?.stop() + keyDownMonitor?.stop() + mouseDownMonitor = nil + keyDownMonitor = nil + appState?.navigationState.isSearchPresented = false + } +} + +private class MenuBarSearchHostingView: NSHostingView { + override var safeAreaInsets: NSEdgeInsets { + NSEdgeInsets() + } + + init( + appState: AppState, + closePanel: @escaping () -> Void + ) { + super.init( + rootView: MenuBarSearchContentView(closePanel: closePanel) + .environmentObject(appState.itemManager) + .environmentObject(appState.imageCache) + .erased() + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @available(*, unavailable) + required init(rootView: AnyView) { + fatalError("init(rootView:) has not been implemented") + } +} + +private struct MenuBarSearchContentView: View { + private typealias ListItem = SectionedListItem + + private enum ItemID: Hashable { + case header(MenuBarSection.Name) + case item(MenuBarItemInfo) + } + + @EnvironmentObject var itemManager: MenuBarItemManager + @State private var searchText = "" + @State private var displayedItems = [SectionedListItem]() + @State private var selection: ItemID? + @FocusState private var searchFieldIsFocused: Bool + + private let fuse = Fuse(threshold: 0.5) + + let closePanel: () -> Void + + var body: some View { + VStack(spacing: 0) { + TextField(text: $searchText, prompt: Text("Search menu bar items…")) { + Text("Search menu bar items…") + } + .labelsHidden() + .textFieldStyle(.plain) + .multilineTextAlignment(.leading) + .font(.system(size: 18)) + .padding(15) + .focused($searchFieldIsFocused) + + Divider() + + SectionedList(selection: $selection, items: $displayedItems) + .contentPadding(8) + .scrollContentBackground(.hidden) + + Divider() + .offset(y: 1) + .zIndex(1) + + HStack { + SettingsButton { + closePanel() + itemManager.appState?.appDelegate?.openSettingsWindow() + } + + Spacer() + + ShowItemButton { + guard + let selection, + let item = menuBarItem(for: selection) + else { + return + } + performAction(for: item) + } + } + .padding(5) + .background(.thinMaterial) + } + .background { + VisualEffectView(material: .sheet, blendingMode: .behindWindow) + .opacity(0.5) + } + .frame(width: 600, height: 400) + .fixedSize() + .task { + searchFieldIsFocused = true + } + .onChange(of: searchText, initial: true) { + updateDisplayedItems() + selectFirstDisplayedItem() + } + .onChange(of: itemManager.itemCache, initial: true) { + updateDisplayedItems() + } + } + + private func selectFirstDisplayedItem() { + selection = displayedItems.first { $0.isSelectable }?.id + } + + private func updateDisplayedItems() { + let searchItems: [(listItem: ListItem, title: String)] = MenuBarSection.Name.allCases.reduce(into: []) { items, section in + let headerItem = ListItem.header(id: .header(section)) { + Text(section.menuString) + .fontWeight(.semibold) + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 10) + } + items.append((headerItem, section.menuString)) + + for item in itemManager.itemCache.managedItems(for: section).reversed() { + let listItem = ListItem.item(id: .item(item.info)) { + performAction(for: item) + } content: { + MenuBarSearchItemView(item: item) + } + items.append((listItem, item.displayName)) + } + } + + if searchText.isEmpty { + displayedItems = searchItems.map { $0.listItem } + } else { + let selectableItems = searchItems.compactMap { searchItem in + if searchItem.listItem.isSelectable { + return searchItem + } + return nil + } + let results = fuse.searchSync(searchText, in: selectableItems.map { $0.title }) + displayedItems = results.map { selectableItems[$0.index].listItem } + } + } + + private func menuBarItem(for selection: ItemID) -> MenuBarItem? { + switch selection { + case .item(let info): + itemManager.itemCache.managedItems.first { $0.info == info } + case .header: + nil + } + } + + private func performAction(for item: MenuBarItem) { + closePanel() + itemManager.tempShowItem(item, clickWhenFinished: true, mouseButton: .left) + } +} + +private struct BottomBarButton: View { + @State private var isHovering = false + + let content: Content + let action: () -> Void + + init(action: @escaping () -> Void, @ViewBuilder content: () -> Content) { + self.action = action + self.content = content() + } + + var body: some View { + content + .padding(3) + .background { + VisualEffectView(material: .selection, blendingMode: .withinWindow) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .circular)) + .opacity(isHovering ? 0.25 : 0) + } + .contentShape(Rectangle()) + .onHover { hovering in + isHovering = hovering + } + .onTapGesture { + action() + } + } +} + +private struct SettingsButton: View { + let action: () -> Void + + var body: some View { + BottomBarButton(action: action) { + Image(.iceCubeStroke) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18, height: 18) + .foregroundStyle(.secondary) + .padding(2) + } + } +} + +private struct ShowItemButton: View { + @State private var isHovering = false + + let action: () -> Void + + var body: some View { + BottomBarButton(action: action) { + HStack { + Text("Show item") + .padding(.horizontal, 5) + + Image(systemName: "return") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 11, height: 11) + .foregroundStyle(.secondary) + .padding(.horizontal, 7) + .padding(.vertical, 5) + .background { + VisualEffectView(material: .selection, blendingMode: .withinWindow) + .clipShape(RoundedRectangle(cornerRadius: 3, style: .circular)) + .opacity(0.5) + } + } + } + } +} + +private let controlCenterIcon: NSImage? = { + guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.controlcenter").first else { + return nil + } + return app.icon +}() + +private struct MenuBarSearchItemView: View { + @EnvironmentObject var imageCache: MenuBarItemImageCache + + let item: MenuBarItem + + private var image: NSImage? { + guard + let image = imageCache.images[item.info], + let screen = imageCache.screen + else { + return nil + } + let size = CGSize( + width: CGFloat(image.width) / screen.backingScaleFactor, + height: CGFloat(image.height) / screen.backingScaleFactor + ) + return NSImage(cgImage: image, size: size) + } + + private var appIcon: NSImage? { + if item.info.namespace == .systemUIServer { + controlCenterIcon + } else { + item.owningApplication?.icon + } + } + + var body: some View { + HStack { + if let appIcon { + Image(nsImage: appIcon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 24, height: 24) + } + Text(item.displayName) + Spacer() + imageViewWithBackground + } + .padding(8) + } + + @ViewBuilder + private var imageViewWithBackground: some View { + if let image { + ZStack { + VisualEffectView(material: .selection, blendingMode: .behindWindow) + .clipShape( + RoundedRectangle(cornerRadius: 5, style: .circular) + ) + .opacity(0.75) + .frame(width: item.frame.width) + .overlay { + RoundedRectangle(cornerRadius: 5, style: .circular) + .inset(by: 0.5) + .stroke(lineWidth: 1) + .foregroundStyle(.white) + .opacity(0.15) + } + + Image(nsImage: image) + .frame(height: 24) + } + } + } +} diff --git a/Ice/MenuBar/Spacing/MenuBarItemSpacingManager.swift b/Ice/MenuBar/Spacing/MenuBarItemSpacingManager.swift new file mode 100644 index 00000000..425f2efc --- /dev/null +++ b/Ice/MenuBar/Spacing/MenuBarItemSpacingManager.swift @@ -0,0 +1,212 @@ +// +// MenuBarItemSpacingManager.swift +// Ice +// + +import Cocoa +import Combine +import OSLog + +/// Manager for menu bar item spacing. +@MainActor +class MenuBarItemSpacingManager { + /// UserDefaults keys. + private enum Key: String { + case spacing = "NSStatusItemSpacing" + case padding = "NSStatusItemSelectionPadding" + + /// The default value for the key. + var defaultValue: Int { + switch self { + case .spacing: 16 + case .padding: 16 + } + } + } + + /// An error that groups multiple failed app relaunches. + private struct GroupedRelaunchError: LocalizedError { + let failedApps: [String] + + var errorDescription: String? { + "The following applications failed to quit and were not restarted:\n" + failedApps.joined(separator: "\n") + } + + var recoverySuggestion: String? { + "You may need to log out for the changes to take effect." + } + } + + /// Delay before force terminating an app. + private let forceTerminateDelay = 1 + + /// The offset to apply to the default spacing and padding. + /// Does not take effect until ``applyOffset()`` is called. + var offset = 0 + + /// Runs a command with the given arguments. + private func runCommand(_ command: String, with arguments: [String]) async throws { + let process = Process() + + process.executableURL = URL(filePath: "/usr/bin/env") + process.arguments = CollectionOfOne(command) + arguments + + let task = Task.detached { + try process.run() + process.waitUntilExit() + } + + return try await task.value + } + + /// Removes the value for the specified key. + private func removeValue(forKey key: Key) async throws { + try await runCommand("defaults", with: ["-currentHost", "delete", "-globalDomain", key.rawValue]) + } + + /// Sets the value for the specified key to the key's default value plus the given offset. + private func setOffset(_ offset: Int, forKey key: Key) async throws { + try await runCommand("defaults", with: ["-currentHost", "write", "-globalDomain", key.rawValue, "-int", String(key.defaultValue + offset)]) + } + + /// Returns a log string for the given app. + private nonisolated func logString(for app: NSRunningApplication) -> String { + app.localizedName ?? app.bundleIdentifier ?? "" + } + + /// Asynchronously signals the given app to quit. + private func signalAppToQuit(_ app: NSRunningApplication) async throws { + if app.isTerminated { + Logger.spacing.debug("Application \"\(self.logString(for: app))\" is already terminated") + return + } else { + Logger.spacing.debug("Signaling application \"\(self.logString(for: app))\" to quit") + } + + app.terminate() + + var cancellable: AnyCancellable? + return try await withCheckedThrowingContinuation { continuation in + let timeoutTask = Task { + try await Task.sleep(for: .seconds(forceTerminateDelay)) + if !app.isTerminated { + Logger.spacing.debug("Application \"\(self.logString(for: app))\" did not terminate within \(self.forceTerminateDelay) seconds, attempting to force terminate") + app.forceTerminate() + } + } + + cancellable = app.publisher(for: \.isTerminated).sink { isTerminated in + guard isTerminated else { + return + } + timeoutTask.cancel() + cancellable?.cancel() + Logger.spacing.debug("Application \"\(self.logString(for: app))\" terminated successfully") + continuation.resume() + } + } + } + + /// Asynchronously launches the app at the given URL. + private nonisolated func launchApp(at applicationURL: URL, bundleIdentifier: String) async throws { + if let app = NSWorkspace.shared.runningApplications.first(where: { $0.bundleIdentifier == bundleIdentifier }) { + Logger.spacing.debug("Application \"\(self.logString(for: app))\" is already open, so skipping launch") + return + } + let configuration = NSWorkspace.OpenConfiguration() + configuration.activates = false + configuration.addsToRecentItems = false + configuration.createsNewApplicationInstance = false + configuration.promptsUserIfNeeded = false + try await NSWorkspace.shared.openApplication(at: applicationURL, configuration: configuration) + } + + /// Asynchronously relaunches the given app. + private func relaunchApp(_ app: NSRunningApplication) async throws { + struct RelaunchError: Error { } + guard + let url = app.bundleURL, + let bundleIdentifier = app.bundleIdentifier + else { + throw RelaunchError() + } + try await signalAppToQuit(app) + if app.isTerminated { + try await launchApp(at: url, bundleIdentifier: bundleIdentifier) + } else { + throw RelaunchError() + } + } + + /// Applies the current ``offset``. + /// + /// - Note: Calling this restarts all apps with a menu bar item. + func applyOffset() async throws { + if offset == 0 { + try await removeValue(forKey: .spacing) + try await removeValue(forKey: .padding) + } else { + try await setOffset(offset, forKey: .spacing) + try await setOffset(offset, forKey: .padding) + } + + try? await Task.sleep(for: .milliseconds(100)) + + let items = MenuBarItem.getMenuBarItemsPrivateAPI(onScreenOnly: false, activeSpaceOnly: true) + let pids = Set(items.map { $0.ownerPID }) + + var failedApps = [String]() + + await withTaskGroup(of: Void.self) { group in + for pid in pids { + guard + let app = NSRunningApplication(processIdentifier: pid), + app.bundleIdentifier != "com.apple.controlcenter", // ControlCenter handles its own relaunch, so skip it. + app != .current + else { + break + } + group.addTask { @MainActor in + do { + try await self.relaunchApp(app) + } catch { + guard let name = app.localizedName else { + return + } + if app.bundleIdentifier == "com.apple.Spotlight" { + // Spotlight automatically relaunches, so only consider it a failure if it never quit. + if + let latestSpotlightInstance = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Spotlight").first, + latestSpotlightInstance.processIdentifier == app.processIdentifier + { + failedApps.append(name) + } + } else { + failedApps.append(name) + } + } + } + } + } + + try? await Task.sleep(for: .milliseconds(100)) + + if let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.controlcenter").first { + do { + try await signalAppToQuit(app) + } catch { + if let name = app.localizedName { + failedApps.append(name) + } + } + } + + if !failedApps.isEmpty { + throw GroupedRelaunchError(failedApps: failedApps) + } + } +} + +private extension Logger { + static let spacing = Logger(category: "Spacing") +} diff --git a/Ice/Permissions/PermissionsView.swift b/Ice/Permissions/PermissionsView.swift index d44fda29..91fc6520 100644 --- a/Ice/Permissions/PermissionsView.swift +++ b/Ice/Permissions/PermissionsView.swift @@ -9,8 +9,6 @@ struct PermissionsView: View { @EnvironmentObject var permissionsManager: PermissionsManager @Environment(\.openWindow) private var openWindow - let onContinue: () -> Void - var body: some View { VStack(spacing: 0) { headerView @@ -24,6 +22,21 @@ struct PermissionsView: View { } .padding(.horizontal) .fixedSize() + .readWindow { window in + guard let window else { + return + } + window.styleMask.remove([.closable, .miniaturizable]) + if let contentView = window.contentView { + with(contentView.safeAreaInsets) { insets in + insets.bottom = -insets.bottom + insets.left = -insets.left + insets.right = -insets.right + insets.top = -insets.top + contentView.additionalSafeAreaInsets = insets + } + } + } } @ViewBuilder @@ -43,7 +56,7 @@ struct PermissionsView: View { @ViewBuilder private var explanationView: some View { - GroupBox { + IceSection { VStack { Text("Ice needs permission to manage the menu bar.") Text("Absolutely no personal information is collected or stored.") @@ -86,7 +99,12 @@ struct PermissionsView: View { @ViewBuilder private var continueButton: some View { Button { - onContinue() + guard let appState = permissionsManager.appState else { + return + } + appState.performSetup() + appState.permissionsWindow?.close() + appState.appDelegate?.openSettingsWindow() } label: { Text("Continue") .frame(maxWidth: .infinity) @@ -96,7 +114,7 @@ struct PermissionsView: View { @ViewBuilder private func permissionBox(_ permission: Permission) -> some View { - GroupBox { + IceSection { VStack(spacing: 10) { Text(permission.title) .font(.title) diff --git a/Ice/Permissions/PermissionsWindow.swift b/Ice/Permissions/PermissionsWindow.swift index 77130b73..957366f6 100644 --- a/Ice/Permissions/PermissionsWindow.swift +++ b/Ice/Permissions/PermissionsWindow.swift @@ -8,34 +8,48 @@ import SwiftUI struct PermissionsWindow: Scene { @ObservedObject var permissionsManager: PermissionsManager - let onContinue: () -> Void - - init(appState: AppState, onContinue: @escaping () -> Void) { + init(appState: AppState) { self.permissionsManager = appState.permissionsManager - self.onContinue = onContinue } var body: some Scene { - Window("Permissions", id: Constants.permissionsWindowID) { - PermissionsView(onContinue: onContinue) - .environmentObject(permissionsManager) - .readWindow { window in - guard let window else { - return - } - window.styleMask.remove([.closable, .miniaturizable]) - if let contentView = window.contentView { - with(contentView.safeAreaInsets) { insets in - insets.bottom = -insets.bottom - insets.left = -insets.left - insets.right = -insets.right - insets.top = -insets.top - contentView.additionalSafeAreaInsets = insets - } - } + permissionsWindow + .windowResizability(.contentSize) + .windowStyle(.hiddenTitleBar) + .environmentObject(permissionsManager) + } + + private var permissionsWindow: some Scene { + if #available(macOS 15.0, *) { + return PermissionsWindowMacOS15() + } else { + return PermissionsWindowMacOS14() + } + } +} + +@available(macOS 14.0, *) +private struct PermissionsWindowMacOS14: Scene { + var body: some Scene { + Window(Constants.permissionsWindowTitle, id: Constants.permissionsWindowID) { + PermissionsView() + } + } +} + +@available(macOS 15.0, *) +private struct PermissionsWindowMacOS15: Scene { + @Environment(\.dismissWindow) private var dismissWindow + @State private var launchBehavior: SceneLaunchBehavior = .presented + + var body: some Scene { + Window(Constants.permissionsWindowTitle, id: Constants.permissionsWindowID) { + PermissionsView() + .once { + dismissWindow(id: Constants.permissionsWindowID) + launchBehavior = .suppressed // Keep the window from reopening. } } - .windowResizability(.contentSize) - .windowStyle(.hiddenTitleBar) + .defaultLaunchBehavior(launchBehavior) } } diff --git a/Ice/Resources/Acknowledgements.pdf b/Ice/Resources/Acknowledgements.pdf index d1ab8b98..8c8d5821 100644 Binary files a/Ice/Resources/Acknowledgements.pdf and b/Ice/Resources/Acknowledgements.pdf differ diff --git a/Ice/Resources/Acknowledgements.rtf b/Ice/Resources/Acknowledgements.rtf index 080e58b1..69f7bc35 100644 --- a/Ice/Resources/Acknowledgements.rtf +++ b/Ice/Resources/Acknowledgements.rtf @@ -1,8 +1,8 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2759 +{\rtf1\ansi\ansicpg1252\cocoartf2761 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 HelveticaNeue-Bold;\f1\fnil\fcharset0 HelveticaNeue;} {\colortbl;\red255\green255\blue255;\red0\green0\blue0;} {\*\expandedcolortbl;;\cssrgb\c0\c1\c1;} -\margl1440\margr1440\vieww23660\viewh12520\viewkind0 +\margl1440\margr1440\vieww34360\viewh20460\viewkind0 \deftab720 \pard\pardeftab720\partightenfactor0 @@ -53,6 +53,26 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\page \ \pard\pardeftab720\partightenfactor0 +\f0\b\fs28 \cf2 Ifrit +\f1\b0 \ +{\field{\*\fldinst{HYPERLINK "https://github.com/ukushu/Ifrit"}}{\fldrslt +\fs24 \ul \ulc2 https://github.com/ukushu/Ifrit}} +\fs22 \ + +\fs24 \ +MIT License\ +\ +Copyright (c) 2024 Andrii Vynnychenko, Kirollos Risk(original "fuse-swift" repository code)\ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\ +\ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ +\pard\pardeftab720\partightenfactor0 +\cf2 \ +\pard\pardeftab720\partightenfactor0 + \f0\b\fs28 \cf2 LaunchAtLogin \f1\b0 \ \pard\pardeftab720\partightenfactor0 diff --git a/Ice/Settings/SettingsManagers/AdvancedSettingsManager.swift b/Ice/Settings/SettingsManagers/AdvancedSettingsManager.swift index 9efe6ee9..99d2b869 100644 --- a/Ice/Settings/SettingsManagers/AdvancedSettingsManager.swift +++ b/Ice/Settings/SettingsManagers/AdvancedSettingsManager.swift @@ -6,6 +6,7 @@ import Combine import Foundation +@MainActor final class AdvancedSettingsManager: ObservableObject { /// Valid modifier keys that can be used to trigger the secondary /// action of all control items. @@ -31,6 +32,9 @@ final class AdvancedSettingsManager: ObservableObject { /// The delay before showing on hover. @Published var showOnHoverDelay: TimeInterval = 0.2 + /// Time interval to temporarily show items for. + @Published var tempShowInterval: TimeInterval = 15 + private var cancellables = Set() private(set) weak var appState: AppState? @@ -50,6 +54,7 @@ final class AdvancedSettingsManager: ObservableObject { Defaults.ifPresent(key: .enableAlwaysHiddenSection, assign: &enableAlwaysHiddenSection) Defaults.ifPresent(key: .canToggleAlwaysHiddenSection, assign: &canToggleAlwaysHiddenSection) Defaults.ifPresent(key: .showOnHoverDelay, assign: &showOnHoverDelay) + Defaults.ifPresent(key: .tempShowInterval, assign: &tempShowInterval) } private func configureCancellables() { @@ -90,6 +95,13 @@ final class AdvancedSettingsManager: ObservableObject { } .store(in: &c) + $tempShowInterval + .receive(on: DispatchQueue.main) + .sink { interval in + Defaults.set(interval, forKey: .tempShowInterval) + } + .store(in: &c) + cancellables = c } } diff --git a/Ice/Settings/SettingsManagers/GeneralSettingsManager.swift b/Ice/Settings/SettingsManagers/GeneralSettingsManager.swift index 39b566b8..c99d9e98 100644 --- a/Ice/Settings/SettingsManagers/GeneralSettingsManager.swift +++ b/Ice/Settings/SettingsManagers/GeneralSettingsManager.swift @@ -7,6 +7,7 @@ import Combine import Foundation import OSLog +@MainActor final class GeneralSettingsManager: ObservableObject { /// A Boolean value that indicates whether the Ice icon /// should be shown. @@ -45,6 +46,9 @@ final class GeneralSettingsManager: ObservableObject { /// menu bar. @Published var showOnScroll = true + /// The offset to apply to the menu bar item spacing and padding. + @Published var itemSpacingOffset: Double = 0 + /// A Boolean value that indicates whether the hidden section /// should automatically rehide. @Published var autoRehide = true @@ -80,6 +84,7 @@ final class GeneralSettingsManager: ObservableObject { Defaults.ifPresent(key: .showOnClick, assign: &showOnClick) Defaults.ifPresent(key: .showOnHover, assign: &showOnHover) Defaults.ifPresent(key: .showOnScroll, assign: &showOnScroll) + Defaults.ifPresent(key: .itemSpacingOffset, assign: &itemSpacingOffset) Defaults.ifPresent(key: .autoRehide, assign: &autoRehide) Defaults.ifPresent(key: .rehideInterval, assign: &rehideInterval) @@ -176,6 +181,14 @@ final class GeneralSettingsManager: ObservableObject { } .store(in: &c) + $itemSpacingOffset + .receive(on: DispatchQueue.main) + .sink { [weak appState] offset in + Defaults.set(offset, forKey: .itemSpacingOffset) + appState?.spacingManager.offset = Int(offset) + } + .store(in: &c) + $autoRehide .receive(on: DispatchQueue.main) .sink { autoRehide in diff --git a/Ice/Settings/SettingsManagers/HotkeySettingsManager.swift b/Ice/Settings/SettingsManagers/HotkeySettingsManager.swift index d063ea99..8ff9c946 100644 --- a/Ice/Settings/SettingsManagers/HotkeySettingsManager.swift +++ b/Ice/Settings/SettingsManagers/HotkeySettingsManager.swift @@ -6,6 +6,7 @@ import Combine import OSLog +@MainActor final class HotkeySettingsManager: ObservableObject { @Published private(set) var hotkeys = HotkeyAction.allCases.map { action in Hotkey(keyCombination: nil, action: action) diff --git a/Ice/Settings/SettingsManagers/SettingsManager.swift b/Ice/Settings/SettingsManagers/SettingsManager.swift index fe2a4973..9c54691a 100644 --- a/Ice/Settings/SettingsManagers/SettingsManager.swift +++ b/Ice/Settings/SettingsManagers/SettingsManager.swift @@ -5,6 +5,7 @@ import Combine +@MainActor final class SettingsManager: ObservableObject { let generalSettingsManager: GeneralSettingsManager diff --git a/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift b/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift index 521247c0..60f34e45 100644 --- a/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/AdvancedSettingsPane.swift @@ -3,11 +3,11 @@ // Ice // -import CompactSlider import SwiftUI struct AdvancedSettingsPane: View { @EnvironmentObject var appState: AppState + @State private var maxSliderLabelWidth: CGFloat = 0 private var menuBarManager: MenuBarManager { appState.menuBarManager @@ -17,9 +17,9 @@ struct AdvancedSettingsPane: View { appState.settingsManager.advancedSettingsManager } - private var showOnHoverDelay: LocalizedStringKey { - let formatted = manager.showOnHoverDelay.formatted() - return if manager.showOnHoverDelay == 1 { + private func formattedToSeconds(_ interval: TimeInterval) -> LocalizedStringKey { + let formatted = interval.formatted() + return if interval == 1 { LocalizedStringKey(formatted + " second") } else { LocalizedStringKey(formatted + " seconds") @@ -27,50 +27,47 @@ struct AdvancedSettingsPane: View { } var body: some View { - Form { - Section { + IceForm { + IceSection { hideApplicationMenus showSectionDividers } - Section("Always-Hidden Section") { + IceSection { enableAlwaysHiddenSection canToggleAlwaysHiddenSection } - Section("Other") { + IceSection { showOnHoverDelaySlider + tempShowIntervalSlider } } - .formStyle(.grouped) - .scrollBounceBehavior(.basedOnSize) } @ViewBuilder private var hideApplicationMenus: some View { - Toggle(isOn: manager.bindings.hideApplicationMenus) { - Text("Hide application menus when showing menu bar items") - Text("Make more room in the menu bar by hiding the left application menus if needed") - } + Toggle("Hide application menus when showing menu bar items", isOn: manager.bindings.hideApplicationMenus) + .annotation("Make more room in the menu bar by hiding the left application menus if needed") } @ViewBuilder private var showSectionDividers: some View { - Toggle(isOn: manager.bindings.showSectionDividers) { - Text("Show section dividers") - HStack(spacing: 2) { - Text("Insert divider items") - if let nsImage = ControlItemImage.builtin(.chevronLarge).nsImage(for: appState) { - HStack(spacing: 0) { - Text("(") - .font(.body.monospaced().bold()) - Image(nsImage: nsImage) - .padding(.horizontal, -2) - Text(")") - .font(.body.monospaced().bold()) + Toggle("Show section dividers", isOn: manager.bindings.showSectionDividers) + .annotation { + HStack(spacing: 2) { + Text("Insert divider items") + if let nsImage = ControlItemImage.builtin(.chevronLarge).nsImage(for: appState) { + HStack(spacing: 0) { + Text("(") + .font(.body.monospaced().bold()) + Image(nsImage: nsImage) + .padding(.horizontal, -2) + Text(")") + .font(.body.monospaced().bold()) + } } + Text("between sections") } - Text("between sections") } - } } @ViewBuilder @@ -81,35 +78,55 @@ struct AdvancedSettingsPane: View { @ViewBuilder private var canToggleAlwaysHiddenSection: some View { if manager.enableAlwaysHiddenSection { - Toggle(isOn: manager.bindings.canToggleAlwaysHiddenSection) { - Text("Always-hidden section can be shown") - if appState.settingsManager.generalSettingsManager.showOnClick { - Text("⌥ + click one of Ice's menu bar items, or inside an empty area of the menu bar to show the section") - } else { - Text("⌥ + click one of Ice's menu bar items to show the section") + Toggle("Always-hidden section can be shown", isOn: manager.bindings.canToggleAlwaysHiddenSection) + .annotation { + if appState.settingsManager.generalSettingsManager.showOnClick { + Text("⌥ + click one of Ice's menu bar items, or inside an empty area of the menu bar to show the section") + } else { + Text("⌥ + click one of Ice's menu bar items to show the section") + } } - } } } @ViewBuilder private var showOnHoverDelaySlider: some View { - LabeledContent { - CompactSlider( + IceLabeledContent { + IceSlider( + formattedToSeconds(manager.showOnHoverDelay), value: manager.bindings.showOnHoverDelay, in: 0...1, - step: 0.1, - handleVisibility: .hovering(width: 1) - ) { - Text(showOnHoverDelay) - .textSelection(.disabled) - } - .compactSliderDisabledHapticFeedback(true) + step: 0.1 + ) } label: { Text("Show on hover delay") .frame(minHeight: .compactSliderMinHeight) - Text("The amount of time to wait before showing on hover") + .frame(minWidth: maxSliderLabelWidth, alignment: .leading) + .onFrameChange { frame in + maxSliderLabelWidth = max(maxSliderLabelWidth, frame.width) + } + } + .annotation("The amount of time to wait before showing on hover") + } + + @ViewBuilder + private var tempShowIntervalSlider: some View { + IceLabeledContent { + IceSlider( + formattedToSeconds(manager.tempShowInterval), + value: manager.bindings.tempShowInterval, + in: 0...30, + step: 1 + ) + } label: { + Text("Temporarily shown item delay") + .frame(minHeight: .compactSliderMinHeight) + .frame(minWidth: maxSliderLabelWidth, alignment: .leading) + .onFrameChange { frame in + maxSliderLabelWidth = max(maxSliderLabelWidth, frame.width) + } } + .annotation("The amount of time to wait before hiding temporarily shown menu bar items") } } diff --git a/Ice/Settings/SettingsPanes/GeneralSettingsPane.swift b/Ice/Settings/SettingsPanes/GeneralSettingsPane.swift index 0dba5ffa..58ad63af 100644 --- a/Ice/Settings/SettingsPanes/GeneralSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/GeneralSettingsPane.swift @@ -3,7 +3,6 @@ // Ice // -import CompactSlider import LaunchAtLogin import SwiftUI @@ -12,42 +11,70 @@ struct GeneralSettingsPane: View { @State private var isImportingCustomIceIcon = false @State private var isPresentingError = false @State private var presentedError: AnyLocalizedError? + @State private var isApplyingOffset = false + @State private var tempItemSpacingOffset: CGFloat = 0 // Temporary state for the slider private var manager: GeneralSettingsManager { appState.settingsManager.generalSettingsManager } - private var rehideInterval: LocalizedStringKey { + private var itemSpacingOffset: LocalizedStringKey { + localizedOffsetString(for: manager.itemSpacingOffset) + } + + private func localizedOffsetString(for offset: CGFloat) -> LocalizedStringKey { + switch offset { + case -16: + return LocalizedStringKey("none") + case 0: + return LocalizedStringKey("default") + case 16: + return LocalizedStringKey("max") + default: + return LocalizedStringKey(offset.formatted()) + } + } + + private var rehideIntervalKey: LocalizedStringKey { let formatted = manager.rehideInterval.formatted() - return if manager.rehideInterval == 1 { - LocalizedStringKey(formatted + " second") + if manager.rehideInterval == 1 { + return LocalizedStringKey(formatted + " second") } else { - LocalizedStringKey(formatted + " seconds") + return LocalizedStringKey(formatted + " seconds") } } + private var hasSpacingSliderValueChanged: Bool { + tempItemSpacingOffset != manager.itemSpacingOffset + } + + private var isActualOffsetDifferentFromDefault: Bool { + manager.itemSpacingOffset != 0 + } + var body: some View { - Form { - Section { + IceForm { + IceSection { launchAtLogin } - Section { + IceSection { iceIconOptions } - Section { + IceSection { iceBarOptions } - Section { + IceSection { showOnClick showOnHover showOnScroll } - Section { + IceSection { autoRehideOptions } + IceSection { + spacingOptions + } } - .formStyle(.grouped) - .scrollBounceBehavior(.basedOnSize) .alert(isPresented: $isPresentingError, error: presentedError) { Button("OK") { presentedError = nil @@ -62,53 +89,54 @@ struct GeneralSettingsPane: View { } @ViewBuilder - private func label(for imageSet: ControlItemImageSet) -> some View { + private func menuItem(for imageSet: ControlItemImageSet) -> some View { Label { Text(imageSet.name.rawValue) } icon: { if let nsImage = imageSet.hidden.nsImage(for: appState) { - Image(nsImage: nsImage) + switch imageSet.name { + case .custom: + Image(size: CGSize(width: 18, height: 18)) { context in + context.draw( + Image(nsImage: nsImage), + in: context.clipBoundingRect + ) + } + default: + Image(nsImage: nsImage) + } } } - .tag(imageSet) + .iceMenuItemAction { + manager.iceIcon = imageSet + } } @ViewBuilder private var iceIconOptions: some View { - Toggle(isOn: manager.bindings.showIceIcon) { - Text("Show Ice icon") - if !manager.showIceIcon { - Text("You can still access Ice's settings by right-clicking an empty area in the menu bar") + Toggle("Show Ice icon", isOn: manager.bindings.showIceIcon) + .annotation { + if !manager.showIceIcon { + Text("You can still access Ice's settings by right-clicking an empty area in the menu bar") + } } - } if manager.showIceIcon { - LabeledContent { - Menu { - Picker("Ice icon", selection: manager.bindings.iceIcon) { - ForEach(ControlItemImageSet.userSelectableIceIcons) { imageSet in - label(for: imageSet) - } - - if let lastCustomIceIcon = manager.lastCustomIceIcon { - label(for: lastCustomIceIcon) - } - } - .pickerStyle(.inline) - .labelsHidden() - - Button("Choose image…") { + IceMenu("Ice icon") { + ForEach(ControlItemImageSet.userSelectableIceIcons) { imageSet in + menuItem(for: imageSet) + } + if let lastCustomIceIcon = manager.lastCustomIceIcon { + menuItem(for: lastCustomIceIcon) + } + Divider() + Text("Choose image…") + .iceMenuItemAction { isImportingCustomIceIcon = true } - } label: { - label(for: manager.iceIcon) - } - .labelStyle(.titleAndIcon) - .scaledToFit() - .fixedSize() - } label: { - Text("Ice icon") - Text("Choose a custom icon to show in the menu bar") + } title: { + menuItem(for: manager.iceIcon) } + .annotation("Choose a custom icon to show in the menu bar") .fileImporter( isPresented: $isImportingCustomIceIcon, allowedContentTypes: [.image] @@ -116,9 +144,7 @@ struct GeneralSettingsPane: View { do { let url = try result.get() if url.startAccessingSecurityScopedResource() { - defer { - url.stopAccessingSecurityScopedResource() - } + defer { url.stopAccessingSecurityScopedResource() } let data = try Data(contentsOf: url) manager.iceIcon = ControlItemImageSet(name: .custom, image: .data(data)) } @@ -129,10 +155,8 @@ struct GeneralSettingsPane: View { } if case .custom = manager.iceIcon.name { - Toggle(isOn: manager.bindings.customIceIconIsTemplate) { - Text("Apply system theme to icon") - Text("Display the icon as a monochrome image matching the system appearance") - } + Toggle("Apply system theme to icon", isOn: manager.bindings.customIceIconIsTemplate) + .annotation("Display the icon as a monochrome image matching the system appearance") } } } @@ -147,20 +171,18 @@ struct GeneralSettingsPane: View { @ViewBuilder private var useIceBar: some View { - Toggle(isOn: manager.bindings.useIceBar) { - Text("Use Ice Bar") - Text("Show hidden menu bar items in a separate bar below the menu bar") - } + Toggle("Use Ice Bar", isOn: manager.bindings.useIceBar) + .annotation("Show hidden menu bar items in a separate bar below the menu bar") } @ViewBuilder private var iceBarLocationPicker: some View { - Picker(selection: manager.bindings.iceBarLocation) { + IcePicker("Location", selection: manager.bindings.iceBarLocation) { ForEach(IceBarLocation.allCases) { location in - Text(location.localized).tag(location) + Text(location.localized).icePickerID(location) } - } label: { - Text("Location") + } + .annotation { switch manager.iceBarLocation { case .default: Text("The Ice Bar's location changes based on context") @@ -174,36 +196,87 @@ struct GeneralSettingsPane: View { @ViewBuilder private var showOnClick: some View { - Toggle(isOn: manager.bindings.showOnClick) { - Text("Show on click") - Text("Click inside an empty area of the menu bar to show hidden menu bar items") - } + Toggle("Show on click", isOn: manager.bindings.showOnClick) + .annotation("Click inside an empty area of the menu bar to show hidden menu bar items") } @ViewBuilder private var showOnHover: some View { - Toggle(isOn: manager.bindings.showOnHover) { - Text("Show on hover") - Text("Hover over an empty area of the menu bar to show hidden menu bar items") - } + Toggle("Show on hover", isOn: manager.bindings.showOnHover) + .annotation("Hover over an empty area of the menu bar to show hidden menu bar items") } @ViewBuilder private var showOnScroll: some View { - Toggle(isOn: manager.bindings.showOnScroll) { - Text("Show on scroll") - Text("Scroll or swipe in the menu bar to toggle hidden menu bar items") + Toggle("Show on scroll", isOn: manager.bindings.showOnScroll) + .annotation("Scroll or swipe in the menu bar to toggle hidden menu bar items") + } + + @ViewBuilder + private var spacingOptions: some View { + IceLabeledContent { + IceSlider( + localizedOffsetString(for: tempItemSpacingOffset), + value: $tempItemSpacingOffset, + in: -16...16, + step: 2 + ) + .disabled(isApplyingOffset) + } label: { + IceLabeledContent { + Button("Apply") { + applyOffset() + } + .help("Apply the current spacing") + .disabled(isApplyingOffset || !hasSpacingSliderValueChanged) + + if isApplyingOffset { + ProgressView() + .progressViewStyle(.circular) + .scaleEffect(0.5) + .frame(width: 15, height: 15) + } else { + Button { + resetOffsetToDefault() + } label: { + Image(systemName: "arrow.counterclockwise.circle.fill") + } + .buttonStyle(.borderless) + .help("Reset to the default spacing") + .disabled(isApplyingOffset || !isActualOffsetDifferentFromDefault) + } + } label: { + HStack { + Text("Menu bar item spacing") + BetaBadge() + } + .offset(y: -3) + } + } + .annotation("Applying this setting will relaunch all apps with menu bar items. Some apps may need to be manually relaunched.") + .annotation(spacing: 10, font: .callout.bold()) { + GroupBox { + Label { + Text("Beta Note: You may need to log out and back in for this setting to apply properly.") + } icon: { + Image(systemName: "exclamationmark.circle") + } + .padding(4) + } + } + .onAppear { + tempItemSpacingOffset = manager.itemSpacingOffset } } @ViewBuilder private var rehideStrategyPicker: some View { - Picker(selection: manager.bindings.rehideStrategy) { + IcePicker("Strategy", selection: manager.bindings.rehideStrategy) { ForEach(RehideStrategy.allCases) { strategy in - Text(strategy.localized).tag(strategy) + Text(strategy.localized).icePickerID(strategy) } - } label: { - Text("Strategy") + } + .annotation { switch manager.rehideStrategy { case .smart: Text("Menu bar items are rehidden using a smart algorithm") @@ -217,28 +290,45 @@ struct GeneralSettingsPane: View { @ViewBuilder private var autoRehideOptions: some View { - Toggle(isOn: manager.bindings.autoRehide) { - Text("Automatically rehide") - } + Toggle("Automatically rehide", isOn: manager.bindings.autoRehide) if manager.autoRehide { if case .timed = manager.rehideStrategy { - VStack(alignment: .trailing) { + VStack { rehideStrategyPicker - CompactSlider( + IceSlider( + rehideIntervalKey, value: manager.bindings.rehideInterval, in: 0...30, - step: 1, - handleVisibility: .hovering(width: 1) - ) { - Text(rehideInterval) - } - .compactSliderDisabledHapticFeedback(true) + step: 1 + ) } } else { rehideStrategyPicker } } } + + /// Apply menu bar spacing offset. + private func applyOffset() { + isApplyingOffset = true + manager.itemSpacingOffset = tempItemSpacingOffset + Task { + do { + try await appState.spacingManager.applyOffset() + } catch { + let alert = NSAlert(error: error) + alert.runModal() + } + isApplyingOffset = false + } + } + + /// Reset menu bar spacing offset to default. + private func resetOffsetToDefault() { + tempItemSpacingOffset = 0 + manager.itemSpacingOffset = tempItemSpacingOffset + applyOffset() + } } #Preview { diff --git a/Ice/Settings/SettingsPanes/HotkeysSettingsPane.swift b/Ice/Settings/SettingsPanes/HotkeysSettingsPane.swift index c3a3357d..9fa05b0b 100644 --- a/Ice/Settings/SettingsPanes/HotkeysSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/HotkeysSettingsPane.swift @@ -13,18 +13,19 @@ struct HotkeysSettingsPane: View { } var body: some View { - Form { - Section("Menu Bar Sections") { + IceForm { + IceSection("Menu Bar Sections") { hotkeyRecorder(forSection: .hidden) hotkeyRecorder(forSection: .alwaysHidden) } - Section("Other") { + IceSection("Menu Bar Items") { + hotkeyRecorder(forAction: .searchMenuBarItems) + } + IceSection("Other") { hotkeyRecorder(forAction: .toggleApplicationMenus) hotkeyRecorder(forAction: .showSectionDividers) } } - .formStyle(.grouped) - .scrollBounceBehavior(.basedOnSize) } @ViewBuilder @@ -40,6 +41,8 @@ struct HotkeysSettingsPane: View { Text("Toggle application menus") case .showSectionDividers: Text("Show section dividers") + case .searchMenuBarItems: + Text("Search menu bar items") } } } diff --git a/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarAppearanceEditor.swift b/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarAppearanceEditor.swift index 9f6b3786..668bda8a 100644 --- a/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarAppearanceEditor.swift +++ b/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarAppearanceEditor.swift @@ -75,32 +75,35 @@ struct MenuBarAppearanceEditor: View { @ViewBuilder private var mainForm: some View { - Form { - Section { + IceForm { + IceSection { tintPicker shadowToggle } - Section { + IceSection { borderToggle borderColor borderWidth } - Section("Menu Bar Shape") { + IceSection("Menu Bar Shape") { shapePicker isInset } if case .settings = location { - Section { - Text("Tip: you can also edit these settings by right-clicking in an empty area of the menu bar") - .font(.callout) - .foregroundStyle(.secondary) - .frame(maxWidth: .infinity, alignment: .center) + IceSection { + AnnotationView( + alignment: .center, + font: .callout.bold() + ) { + Label { + Text("Tip: you can also edit these settings by right-clicking in an empty area of the menu bar") + } icon: { + Image(systemName: "lightbulb") + } + } } } } - .formStyle(.grouped) - .scrollContentBackground(.hidden) - .scrollBounceBehavior(.basedOnSize) } @ViewBuilder @@ -112,11 +115,11 @@ struct MenuBarAppearanceEditor: View { @ViewBuilder private var tintPicker: some View { - LabeledContent("Tint") { + IceLabeledContent("Tint") { HStack { - Picker("Tint", selection: appearanceManager.bindings.configuration.tintKind) { + IcePicker("Tint", selection: appearanceManager.bindings.configuration.tintKind) { ForEach(MenuBarTintKind.allCases) { tintKind in - Text(tintKind.localized).tag(tintKind) + Text(tintKind.localized).icePickerID(tintKind) } } .labelsHidden() @@ -156,7 +159,7 @@ struct MenuBarAppearanceEditor: View { @ViewBuilder private var borderColor: some View { if appearanceManager.configuration.hasBorder { - LabeledContent("Border Color") { + IceLabeledContent("Border Color") { CustomColorPicker( selection: appearanceManager.bindings.configuration.borderColor, supportsOpacity: true, @@ -169,13 +172,13 @@ struct MenuBarAppearanceEditor: View { @ViewBuilder private var borderWidth: some View { if appearanceManager.configuration.hasBorder { - Picker( + IcePicker( "Border Width", selection: appearanceManager.bindings.configuration.borderWidth ) { - Text("1").tag(1.0) - Text("2").tag(2.0) - Text("3").tag(3.0) + Text("1").icePickerID(1.0) + Text("2").icePickerID(2.0) + Text("3").icePickerID(3.0) } } } @@ -188,7 +191,10 @@ struct MenuBarAppearanceEditor: View { @ViewBuilder private var isInset: some View { if appearanceManager.configuration.shapeKind != .none { - Toggle("Use inset shape on screens with notch", isOn: appearanceManager.bindings.configuration.isInset) + Toggle( + "Use inset shape on screens with notch", + isOn: appearanceManager.bindings.configuration.isInset + ) } } } diff --git a/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarShapePicker.swift b/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarShapePicker.swift index 1f2e00da..be73afc9 100644 --- a/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarShapePicker.swift +++ b/Ice/Settings/SettingsPanes/MenuBarAppearanceSettingsPane/MenuBarShapePicker.swift @@ -16,15 +16,15 @@ struct MenuBarShapePicker: View { @ViewBuilder private var shapeKindPicker: some View { - Picker("Shape Kind", selection: appearanceManager.bindings.configuration.shapeKind) { + IcePicker("Shape Kind", selection: appearanceManager.bindings.configuration.shapeKind) { ForEach(MenuBarShapeKind.allCases, id: \.self) { shape in switch shape { case .none: - Text("None").tag(shape) + Text("None").icePickerID(shape) case .full: - Text("Full").tag(shape) + Text("Full").icePickerID(shape) case .split: - Text("Split").tag(shape) + Text("Split").icePickerID(shape) } } } diff --git a/Ice/Settings/SettingsPanes/MenuBarItemsSettingsPane.swift b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift similarity index 63% rename from Ice/Settings/SettingsPanes/MenuBarItemsSettingsPane.swift rename to Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift index 3f559d7e..bef3955e 100644 --- a/Ice/Settings/SettingsPanes/MenuBarItemsSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/MenuBarLayoutSettingsPane.swift @@ -1,41 +1,46 @@ // -// MenuBarItemsSettingsPane.swift +// MenuBarLayoutSettingsPane.swift // Ice // import SwiftUI -struct MenuBarItemsSettingsPane: View { +struct MenuBarLayoutSettingsPane: View { @EnvironmentObject var appState: AppState var body: some View { if appState.menuBarManager.isMenuBarHiddenBySystemUserDefaults { cannotArrange } else { - ScrollView { - VStack(alignment: .leading, spacing: 20) { - headerText - layoutBars - Spacer() - } - .padding() + IceForm(alignment: .leading, spacing: 20) { + header + layoutBars } - .scrollBounceBehavior(.basedOnSize) } } @ViewBuilder - private var headerText: some View { + private var header: some View { Text("Drag to arrange your menu bar items") .font(.title2) - .annotation { - Text("Tip: you can also arrange menu bar items by ⌘ + dragging them in the menu bar") + + IceSection { + AnnotationView( + alignment: .center, + font: .callout.bold() + ) { + Label { + Text("Tip: you can also arrange menu bar items by ⌘ + dragging them in the menu bar") + } icon: { + Image(systemName: "lightbulb") + } } + } } @ViewBuilder private var layoutBars: some View { - VStack(spacing: 30) { + VStack(spacing: 25) { ForEach(MenuBarSection.Name.allCases, id: \.self) { section in layoutBar(for: section) } @@ -56,9 +61,9 @@ struct MenuBarItemsSettingsPane: View { section.isEnabled { VStack(alignment: .leading, spacing: 2) { - Text(section.name.menuString + " Menu Bar Items") - .font(.system(size: 15)) - .padding(.leading, 2.5) + Text(section.name.menuString + " Section") + .font(.system(size: 14)) + LayoutBar(section: section) .environmentObject(appState.imageCache) } diff --git a/Ice/Settings/SettingsPanes/UpdatesSettingsPane.swift b/Ice/Settings/SettingsPanes/UpdatesSettingsPane.swift index 38ff1df8..2608661f 100644 --- a/Ice/Settings/SettingsPanes/UpdatesSettingsPane.swift +++ b/Ice/Settings/SettingsPanes/UpdatesSettingsPane.swift @@ -21,19 +21,17 @@ struct UpdatesSettingsPane: View { } var body: some View { - Form { - Section { + IceForm { + IceSection { automaticallyCheckForUpdates automaticallyDownloadUpdates } if updatesManager.canCheckForUpdates { - Section { + IceSection { checkForUpdates } } } - .formStyle(.grouped) - .scrollBounceBehavior(.basedOnSize) } @ViewBuilder diff --git a/Ice/Settings/SettingsView.swift b/Ice/Settings/SettingsView.swift index 2c1d304a..80a34174 100644 --- a/Ice/Settings/SettingsView.swift +++ b/Ice/Settings/SettingsView.swift @@ -15,6 +15,12 @@ struct SettingsView: View { detailView } .navigationTitle(navigationState.settingsNavigationIdentifier.localized) + .frame(minWidth: 825, minHeight: 500) + .background { + VisualEffectView(material: .contentBackground, blendingMode: .behindWindow) + .opacity(0.25) + .blendMode(.softLight) + } } @ViewBuilder @@ -25,20 +31,14 @@ struct SettingsView: View { sidebarItem(for: identifier) } } header: { - HStack { - Image(.iceCubeStroke) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) - - Text("Ice") - .font(.system(size: 30, weight: .medium)) - } - .foregroundStyle(.primary) - .padding(.vertical, 8) + Text("Ice") + .font(.system(size: 36, weight: .medium)) + .foregroundStyle(.primary) + .padding(.vertical, 5) } .collapsible(false) } + .scrollDisabled(true) .removeSidebarToggle() .navigationSplitViewColumnWidth(210) } @@ -48,8 +48,8 @@ struct SettingsView: View { switch navigationState.settingsNavigationIdentifier { case .general: GeneralSettingsPane() - case .menuBarItems: - MenuBarItemsSettingsPane() + case .menuBarLayout: + MenuBarLayoutSettingsPane() case .menuBarAppearance: MenuBarAppearanceSettingsPane() case .hotkeys: @@ -71,7 +71,6 @@ struct SettingsView: View { .padding(.leading, 2) } icon: { icon(for: identifier).view - .foregroundStyle(.primary) } .frame(height: 32) } @@ -79,8 +78,8 @@ struct SettingsView: View { private func icon(for identifier: SettingsNavigationIdentifier) -> IconResource { switch identifier { case .general: .systemSymbol("gearshape") - case .menuBarItems: .systemSymbol("menubar.rectangle") - case .menuBarAppearance: .systemSymbol("paintpalette") + case .menuBarLayout: .systemSymbol("rectangle.topthird.inset.filled") + case .menuBarAppearance: .systemSymbol("swatchpalette") case .hotkeys: .systemSymbol("keyboard") case .advanced: .systemSymbol("gearshape.2") case .updates: .systemSymbol("arrow.triangle.2.circlepath.circle") diff --git a/Ice/Settings/SettingsWindow.swift b/Ice/Settings/SettingsWindow.swift index 7b5efc8b..d345d57f 100644 --- a/Ice/Settings/SettingsWindow.swift +++ b/Ice/Settings/SettingsWindow.swift @@ -7,18 +7,52 @@ import SwiftUI struct SettingsWindow: Scene { @ObservedObject var appState: AppState - let onAppear: () -> Void var body: some Scene { - Window("Ice", id: Constants.settingsWindowID) { + settingsWindow + .commandsRemoved() + .windowResizability(.contentSize) + .defaultSize(width: 900, height: 625) + .environmentObject(appState) + .environmentObject(appState.navigationState) + } + + private var settingsWindow: some Scene { + if #available(macOS 15.0, *) { + return SettingsWindowMacOS15() + } else { + return SettingsWindowMacOS14() + } + } +} + +@available(macOS 14.0, *) +private struct SettingsWindowMacOS14: Scene { + @Environment(\.openWindow) private var openWindow + + var body: some Scene { + Window(Constants.settingsWindowTitle, id: Constants.settingsWindowID) { + SettingsView() + .once { + openWindow(id: Constants.permissionsWindowID) + } + } + } +} + +@available(macOS 15.0, *) +private struct SettingsWindowMacOS15: Scene { + @Environment(\.dismissWindow) private var dismissWindow + @State private var launchBehavior: SceneLaunchBehavior = .presented + + var body: some Scene { + Window(Constants.settingsWindowTitle, id: Constants.settingsWindowID) { SettingsView() - .frame(minWidth: 825, minHeight: 500) - .onAppear(perform: onAppear) - .environmentObject(appState) - .environmentObject(appState.navigationState) + .once { + dismissWindow(id: Constants.settingsWindowID) + launchBehavior = .suppressed // Keep the window from reopening. + } } - .commandsRemoved() - .windowResizability(.contentSize) - .defaultSize(width: 900, height: 615) + .defaultLaunchBehavior(launchBehavior) } } diff --git a/Ice/IceBar/IceBar.swift b/Ice/UI/IceBar/IceBar.swift similarity index 100% rename from Ice/IceBar/IceBar.swift rename to Ice/UI/IceBar/IceBar.swift diff --git a/Ice/IceBar/IceBarColorManager.swift b/Ice/UI/IceBar/IceBarColorManager.swift similarity index 100% rename from Ice/IceBar/IceBarColorManager.swift rename to Ice/UI/IceBar/IceBarColorManager.swift diff --git a/Ice/IceBar/IceBarLocation.swift b/Ice/UI/IceBar/IceBarLocation.swift similarity index 100% rename from Ice/IceBar/IceBarLocation.swift rename to Ice/UI/IceBar/IceBarLocation.swift diff --git a/Ice/UI/IceUI/IceForm.swift b/Ice/UI/IceUI/IceForm.swift new file mode 100644 index 00000000..a5619203 --- /dev/null +++ b/Ice/UI/IceUI/IceForm.swift @@ -0,0 +1,52 @@ +// +// IceForm.swift +// Ice +// + +import SwiftUI + +struct IceForm: View { + private let alignment: HorizontalAlignment + private let padding: CGFloat + private let spacing: CGFloat + private let content: Content + + init( + alignment: HorizontalAlignment = .center, + padding: CGFloat = 20, + spacing: CGFloat = 10, + @ViewBuilder content: () -> Content + ) { + self.alignment = alignment + self.padding = padding + self.spacing = spacing + self.content = content() + } + + var body: some View { + ScrollView { + VStack(alignment: alignment, spacing: spacing) { + content + } + .padding(padding) + } + .scrollContentBackground(.hidden) + .scrollBounceBehavior(.basedOnSize) + .toggleStyle(IceFormToggleStyle()) + } +} + +private struct IceFormToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + IceLabeledContent { + Toggle(isOn: configuration.$isOn) { + configuration.label + } + .labelsHidden() + .toggleStyle(.switch) + .controlSize(.mini) + } label: { + configuration.label + } + } +} diff --git a/Ice/UI/IceUI/IceLabeledContent.swift b/Ice/UI/IceUI/IceLabeledContent.swift new file mode 100644 index 00000000..63740c13 --- /dev/null +++ b/Ice/UI/IceUI/IceLabeledContent.swift @@ -0,0 +1,41 @@ +// +// IceLabeledContent.swift +// Ice +// + +import SwiftUI + +struct IceLabeledContent: View { + private let label: Label + private let content: Content + + init( + @ViewBuilder content: () -> Content, + @ViewBuilder label: () -> Label + ) { + self.label = label() + self.content = content() + } + + init( + _ titleKey: LocalizedStringKey, + @ViewBuilder content: () -> Content + ) where Label == Text { + self.init { + content() + } label: { + Text(titleKey) + } + } + + var body: some View { + LabeledContent { + content + .layoutPriority(1) + } label: { + label + .frame(maxWidth: .infinity, alignment: .leading) + .layoutPriority(0) + } + } +} diff --git a/Ice/UI/IceUI/IceMenu.swift b/Ice/UI/IceUI/IceMenu.swift new file mode 100644 index 00000000..bdc8890a --- /dev/null +++ b/Ice/UI/IceUI/IceMenu.swift @@ -0,0 +1,167 @@ +// +// IceMenu.swift +// Ice +// + +import SwiftUI +import OSLog + +struct IceMenu: View { + @State private var isHovering = false + + private let title: Title + private let label: Label + private let content: Content + + /// Creates a menu with the given content, title, and label. + /// + /// - Parameters: + /// - content: A group of menu items. + /// - title: A view to display inside the menu. + /// - label: A view to display as an external label for the menu. + init( + @ViewBuilder content: () -> Content, + @ViewBuilder title: () -> Title, + @ViewBuilder label: () -> Label + ) { + self.title = title() + self.label = label() + self.content = content() + } + + /// Creates a menu with the given content, title, and label key. + /// + /// - Parameters: + /// - labelKey: A string key for the menu's external label. + /// - content: A group of menu items. + /// - title: A view to display inside the menu. + init( + _ labelKey: LocalizedStringKey, + @ViewBuilder content: () -> Content, + @ViewBuilder title: () -> Title + ) where Label == Text { + self.init { + content() + } title: { + title() + } label: { + Text(labelKey) + } + } + + var body: some View { + IceLabeledContent { + ZStack { + IceMenuButtonView() + .opacity(isHovering ? 1 : 0) + .allowsHitTesting(false) + + _VariadicView.Tree(IceMenuLayout(title: title)) { + content + } + .blendMode(.destinationOver) + + HStack(spacing: 5) { + title + .offset(y: -0.5) + + IcePopUpIndicator(isHovering: isHovering, isBordered: true, style: .pullDown) + } + .allowsHitTesting(false) + .padding(.trailing, 2) + .padding(.leading, 10) + } + .fixedSize() + .onHover { hovering in + isHovering = hovering + } + } label: { + label + } + } +} + +private struct IceMenuButtonView: NSViewRepresentable { + func makeNSView(context: Context) -> NSButton { + let button = NSButton() + button.title = "" + return button + } + + func updateNSView(_ nsView: NSButton, context: Context) { } +} + +private struct IceMenuLayout: _VariadicView_UnaryViewRoot { + let title: Title + + func body(children: _VariadicView.Children) -> some View { + Menu { + ForEach(children) { child in + IceMenuItem(child: child) + } + } label: { + title + } + .menuStyle(.borderlessButton) + .menuIndicator(.hidden) + .labelStyle(.titleAndIcon) + } +} + +private class IceMenuItemAction: Hashable { + static let nullAction = IceMenuItemAction { + Logger.iceMenu.warning("No action assigned to menu item") + } + + let body: () -> Void + + init(body: @escaping () -> Void) { + self.body = body + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + static func == (lhs: IceMenuItemAction, rhs: IceMenuItemAction) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +private struct IceMenuItemActionKey: PreferenceKey { + static let defaultValue = IceMenuItemAction.nullAction + + static func reduce(value: inout IceMenuItemAction, nextValue: () -> IceMenuItemAction) { + value = nextValue() + } +} + +private struct IceMenuItem: View { + @State private var action = IceMenuItemAction.nullAction + + let child: _VariadicView.Children.Element + + var body: some View { + Button { + action.body() + } label: { + child + } + .onPreferenceChange(IceMenuItemActionKey.self) { action in + self.action = action + } + } +} + +extension View { + /// Adds an action to perform when this view is clicked inside an ``IceMenu``. + /// + /// - Parameter action: An action to perform. + func iceMenuItemAction(_ action: @escaping () -> Void) -> some View { + preference(key: IceMenuItemActionKey.self, value: IceMenuItemAction(body: action)) + } +} + +private extension Logger { + static let iceMenu = Logger(category: "IceMenu") +} diff --git a/Ice/UI/IceUI/IcePicker.swift b/Ice/UI/IceUI/IcePicker.swift new file mode 100644 index 00000000..1633bf2f --- /dev/null +++ b/Ice/UI/IceUI/IcePicker.swift @@ -0,0 +1,107 @@ +// +// IcePicker.swift +// Ice +// + +import SwiftUI + +struct IcePicker: View { + @Binding var selection: SelectionValue + @State private var isHovering = false + + let label: Label + let content: Content + + init( + selection: Binding, + @ViewBuilder content: () -> Content, + @ViewBuilder label: () -> Label + ) { + self._selection = selection + self.label = label() + self.content = content() + } + + init( + _ titleKey: LocalizedStringKey, + selection: Binding, + @ViewBuilder content: () -> Content + ) where Label == Text { + self.init(selection: selection) { + content() + } label: { + Text(titleKey) + } + } + + var body: some View { + IceLabeledContent { + ZStack { + IcePickerButtonView() + .opacity(isHovering ? 1 : 0) + .allowsHitTesting(false) + + Picker(selection: $selection) { + content + .labelStyle(.titleAndIcon) + } label: { + label + } + .labelsHidden() + .pickerStyle(.menu) + .buttonStyle(.plain) + .menuIndicator(.hidden) + .blendMode(.destinationOver) + + HStack(spacing: 5) { + _VariadicView.Tree(IcePickerLayout(selection: $selection)) { + content + .labelStyle(.titleAndIcon) + } + .offset(y: -0.5) + + IcePopUpIndicator(isHovering: isHovering, isBordered: true, style: .popUp) + } + .allowsHitTesting(false) + .padding(.trailing, 2) + .padding(.leading, 10) + } + .fixedSize() + .onHover { hovering in + isHovering = hovering + } + } label: { + label + } + } +} + +private struct IcePickerButtonView: NSViewRepresentable { + func makeNSView(context: Context) -> NSButton { + let button = NSButton() + button.title = "" + return button + } + + func updateNSView(_ nsView: NSButton, context: Context) { } +} + +private struct IcePickerLayout: _VariadicView_UnaryViewRoot { + @Binding var selection: SelectionValue + + @ViewBuilder + func body(children: _VariadicView.Children) -> some View { + if let child = children.first(where: { $0.id() == selection }) { + child + } + } +} + +extension View { + /// Binds the identity of an item in an ``IcePicker`` to the given value. + /// + /// - Parameter id: A `Hashable` value to use as the view's identity. + func icePickerID(_ id: ID) -> some View { + tag(id).id(id) + } +} diff --git a/Ice/UI/IceUI/IcePopUpIndicator.swift b/Ice/UI/IceUI/IcePopUpIndicator.swift new file mode 100644 index 00000000..a000f31c --- /dev/null +++ b/Ice/UI/IceUI/IcePopUpIndicator.swift @@ -0,0 +1,51 @@ +// +// IcePopUpIndicator.swift +// Ice +// + +import SwiftUI + +struct IcePopUpIndicator: View { + private enum ChevronKind: String { + case up, down + } + + enum Style { + case popUp, pullDown + } + + let isHovering: Bool + let isBordered: Bool + let style: Style + + var body: some View { + ZStack { + if isBordered { + RoundedRectangle(cornerRadius: 4, style: .circular) + .fill(.quaternary) + .opacity(isHovering ? 0 : 1) + } + + switch style { + case .popUp: + VStack(spacing: 2) { + chevron(.up) + chevron(.down) + } + case .pullDown: + chevron(.down) + .offset(y: 0.75) + } + } + .frame(width: 16, height: 16) + } + + @ViewBuilder + private func chevron(_ kind: ChevronKind) -> some View { + Image(systemName: "chevron.\(kind.rawValue)") + .resizable() + .frame(width: 7.5, height: 5) + .fontWeight(.black) + .foregroundStyle(Color.primary) + } +} diff --git a/Ice/UI/IceUI/IceSection.swift b/Ice/UI/IceUI/IceSection.swift new file mode 100644 index 00000000..6872b18f --- /dev/null +++ b/Ice/UI/IceUI/IceSection.swift @@ -0,0 +1,115 @@ +// +// IceSection.swift +// Ice +// + +import SwiftUI + +struct IceSection: View { + private let header: Header + private let content: Content + private let footer: Footer + private let spacing: CGFloat = 10 + + init( + @ViewBuilder header: () -> Header, + @ViewBuilder content: () -> Content, + @ViewBuilder footer: () -> Footer + ) { + self.header = header() + self.content = content() + self.footer = footer() + } + + init( + @ViewBuilder content: () -> Content, + @ViewBuilder footer: () -> Footer + ) where Header == EmptyView { + self.init { + EmptyView() + } content: { + content() + } footer: { + footer() + } + } + + init( + @ViewBuilder header: () -> Header, + @ViewBuilder content: () -> Content + ) where Footer == EmptyView { + self.init { + header() + } content: { + content() + } footer: { + EmptyView() + } + } + + init( + @ViewBuilder content: () -> Content + ) where Header == EmptyView, Footer == EmptyView { + self.init { + EmptyView() + } content: { + content() + } footer: { + EmptyView() + } + } + + init( + _ title: LocalizedStringKey, + @ViewBuilder content: () -> Content + ) where Header == Text, Footer == EmptyView { + self.init { + Text(title) + .font(.headline) + } content: { + content() + } + } + + var body: some View { + VStack(alignment: .leading) { + header + _VariadicView.Tree(IceSectionLayout(spacing: spacing)) { + content + .frame(maxWidth: .infinity) + } + .background { + backgroundShape + .fill(.quinary) + .overlay { + backgroundShape + .stroke(.quaternary) + } + } + footer + } + } + + @ViewBuilder + private var backgroundShape: some Shape { + RoundedRectangle(cornerRadius: 7, style: .circular) + } +} + +private struct IceSectionLayout: _VariadicView_UnaryViewRoot { + let spacing: CGFloat + + @ViewBuilder + func body(children: _VariadicView.Children) -> some View { + let last = children.last?.id + VStack(alignment: .leading, spacing: spacing) { + ForEach(children) { child in + child + if child.id != last { + Divider() + } + } + } + .padding(spacing) + } +} diff --git a/Ice/UI/IceUI/IceSlider.swift b/Ice/UI/IceUI/IceSlider.swift new file mode 100644 index 00000000..4d2f911e --- /dev/null +++ b/Ice/UI/IceUI/IceSlider.swift @@ -0,0 +1,59 @@ +// +// IceSlider.swift +// Ice +// + +import CompactSlider +import SwiftUI + +struct IceSlider: View { + private let value: Binding + private let bounds: ClosedRange + private let step: Value + private let valueLabel: ValueLabel + private let valueLabelSelectability: ValueLabelSelectability + + init( + value: Binding, + in bounds: ClosedRange = 0...1, + step: Value = 0, + valueLabelSelectability: ValueLabelSelectability = .disabled, + @ViewBuilder valueLabel: () -> ValueLabel + ) { + self.value = value + self.bounds = bounds + self.step = step + self.valueLabel = valueLabel() + self.valueLabelSelectability = valueLabelSelectability + } + + init( + _ valueLabelKey: LocalizedStringKey, + valueLabelSelectability: ValueLabelSelectability = .disabled, + value: Binding, + in bounds: ClosedRange = 0...1, + step: Value = 0 + ) where ValueLabel == Text { + self.init( + value: value, + in: bounds, + step: step, + valueLabelSelectability: valueLabelSelectability + ) { + Text(valueLabelKey) + } + } + + var body: some View { + CompactSlider( + value: value, + in: bounds, + step: step, + handleVisibility: .hovering(width: 1) + ) { + valueLabel + .textSelection(valueLabelSelectability) + } + .compactSliderDisabledHapticFeedback(true) + } +} diff --git a/Ice/LayoutBar/LayoutBar.swift b/Ice/UI/LayoutBar/LayoutBar.swift similarity index 84% rename from Ice/LayoutBar/LayoutBar.swift rename to Ice/UI/LayoutBar/LayoutBar.swift index 0f6d6322..9c20d2ae 100644 --- a/Ice/LayoutBar/LayoutBar.swift +++ b/Ice/UI/LayoutBar/LayoutBar.swift @@ -40,7 +40,11 @@ struct LayoutBar: View { .frame(height: 50) .frame(maxWidth: .infinity) .layoutBarStyle(appState: appState, averageColorInfo: menuBarManager.averageColorInfo) - .clipShape(RoundedRectangle(cornerRadius: 9, style: .circular)) + .clipShape(roundedRectangle) + .overlay { + roundedRectangle + .stroke(.quaternary) + } } @ViewBuilder @@ -52,4 +56,9 @@ struct LayoutBar: View { Representable(appState: appState, section: section, spacing: spacing) } } + + @ViewBuilder + private var roundedRectangle: some Shape { + RoundedRectangle(cornerRadius: 9, style: .circular) + } } diff --git a/Ice/LayoutBar/LayoutBarContainer.swift b/Ice/UI/LayoutBar/LayoutBarContainer.swift similarity index 100% rename from Ice/LayoutBar/LayoutBarContainer.swift rename to Ice/UI/LayoutBar/LayoutBarContainer.swift diff --git a/Ice/LayoutBar/LayoutBarItemView.swift b/Ice/UI/LayoutBar/LayoutBarItemView.swift similarity index 100% rename from Ice/LayoutBar/LayoutBarItemView.swift rename to Ice/UI/LayoutBar/LayoutBarItemView.swift diff --git a/Ice/LayoutBar/LayoutBarPaddingView.swift b/Ice/UI/LayoutBar/LayoutBarPaddingView.swift similarity index 100% rename from Ice/LayoutBar/LayoutBarPaddingView.swift rename to Ice/UI/LayoutBar/LayoutBarPaddingView.swift diff --git a/Ice/LayoutBar/LayoutBarScrollView.swift b/Ice/UI/LayoutBar/LayoutBarScrollView.swift similarity index 100% rename from Ice/LayoutBar/LayoutBarScrollView.swift rename to Ice/UI/LayoutBar/LayoutBarScrollView.swift diff --git a/Ice/UI/Pickers/CustomGradientPicker/CustomGradientPicker.swift b/Ice/UI/Pickers/CustomGradientPicker/CustomGradientPicker.swift index db8fa0de..30a61291 100644 --- a/Ice/UI/Pickers/CustomGradientPicker/CustomGradientPicker.swift +++ b/Ice/UI/Pickers/CustomGradientPicker/CustomGradientPicker.swift @@ -60,12 +60,6 @@ struct CustomGradientPicker: View { .onChange(of: gradient) { _, newValue in gradientChanged(to: newValue) } - .onKeyDown(key: .escape) { - selectedStop = nil - } - .onKeyDown(key: .delete) { - deleteSelectedStop() - } .readWindow(window: $window) } @@ -106,7 +100,22 @@ struct CustomGradientPicker: View { private func selectionReader(geometry: GeometryProxy) -> some View { Color.clear .localEventMonitor(mask: .leftMouseDown) { event in - leftMouseDown(with: event, in: geometry) + guard + let window = event.window, + self.window === window + else { + return event + } + let locationInWindow = event.locationInWindow + guard window.contentLayoutRect.contains(locationInWindow) else { + return event + } + let globalFrame = geometry.frame(in: .global) + let flippedLocation = CGPoint(x: locationInWindow.x, y: window.frame.height - locationInWindow.y) + if !globalFrame.contains(flippedLocation) { + selectedStop = nil + } + return event } } @@ -115,23 +124,20 @@ struct CustomGradientPicker: View { Color.clear .contentShape(borderShape) .gesture( - DragGesture( - minimumDistance: 0, - coordinateSpace: .local - ) - .onEnded { value in - guard abs(value.translation.width) <= 2 else { - return - } - let frame = geometry.frame(in: .local) - guard frame.contains(value.location) else { - return + DragGesture(minimumDistance: 0, coordinateSpace: .local) + .onEnded { value in + guard abs(value.translation.width) <= 2 else { + return + } + let frame = geometry.frame(in: .local) + guard frame.contains(value.location) else { + return + } + let x = value.location.x + let width = frame.width - 10 + let location = (x / width) - (6 / width) + insertStop(at: location, select: true) } - let x = value.location.x - let width = frame.width - 10 - let location = (x / width) - (6 / width) - insertStop(at: location) - } ) } @@ -151,31 +157,26 @@ struct CustomGradientPicker: View { } } - /// Inserts a new stop with the appropriate color - /// at the given location in the gradient. - private func insertStop(at location: CGFloat) { + /// Inserts a new stop with the appropriate color at the given location + /// in the gradient. + private func insertStop(at location: CGFloat, select: Bool) { var location = location.clamped(to: 0...1) if (0.48...0.52).contains(location) { location = 0.5 } - let newStop: ColorStop - if + let newStop: ColorStop = if !gradient.stops.isEmpty, let color = gradient.color(at: location) { - newStop = ColorStop( - color: color, - location: location - ) + ColorStop(color: color, location: location) } else { - newStop = ColorStop( - color: CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1), - location: location - ) + ColorStop(color: .black, location: location) } gradient.stops.append(newStop) - DispatchQueue.main.async { - self.selectedStop = newStop + if select { + DispatchQueue.main.async { + self.selectedStop = newStop + } } } @@ -199,42 +200,6 @@ struct CustomGradientPicker: View { self.gradient = gradient } } - - private func deleteSelectedStop() { - guard - let selectedStop, - let index = gradient.stops.firstIndex(of: selectedStop) - else { - return - } - gradient.stops.remove(at: index) - self.selectedStop = nil - } - - private func leftMouseDown( - with event: NSEvent, - in geometry: GeometryProxy - ) -> NSEvent? { - guard - let window = event.window, - self.window === window - else { - return event - } - let locationInWindow = event.locationInWindow - guard window.contentLayoutRect.contains(locationInWindow) else { - return event - } - let globalFrame = geometry.frame(in: .global) - let flippedLocation = CGPoint( - x: locationInWindow.x, - y: window.frame.height - locationInWindow.y - ) - if !globalFrame.contains(flippedLocation) { - selectedStop = nil - } - return event - } } private struct CustomGradientPickerHandle: View { @@ -243,6 +208,7 @@ private struct CustomGradientPickerHandle: View { @Binding var zOrderedStops: [ColorStop] @Binding var cancellables: Set @State private var canActivate = true + @FocusState private var isFocused: Bool let index: Int let supportsOpacity: Bool @@ -326,6 +292,16 @@ private struct CustomGradientPickerHandle: View { } } } + .focusable() + .focusEffectDisabled() + .focused($isFocused) + .onKeyPress(.escape) { + selectedStop = nil + return .handled + } + .onDeleteCommand { + deleteSelectedStop() + } } } @@ -396,6 +372,8 @@ private struct CustomGradientPickerHandle: View { zOrderedStops.append(zOrderedStops.remove(at: index)) } + isFocused = true + var c = Set() NSColorPanel.shared.publisher(for: \.color) @@ -433,6 +411,18 @@ private struct CustomGradientPickerHandle: View { cancellable.cancel() } cancellables.removeAll() + isFocused = false + } + + private func deleteSelectedStop() { + guard + let selectedStop, + let index = gradient.stops.firstIndex(of: selectedStop) + else { + return + } + gradient.stops.remove(at: index) + self.selectedStop = nil } } diff --git a/Ice/UI/ViewModifiers/Annotation.swift b/Ice/UI/ViewModifiers/Annotation.swift deleted file mode 100644 index 7afa7f1a..00000000 --- a/Ice/UI/ViewModifiers/Annotation.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// Annotation.swift -// Ice -// - -import SwiftUI - -extension View { - /// Adds the given view as an annotation below this view. - /// - /// - Parameters: - /// - alignment: The guide for aligning the annotation content - /// horizontally with this view. - /// - spacing: The vertical spacing between this view and the - /// annotation content. Pass `nil` to use the default spacing. - /// - font: The font to apply to the annotation content's environment. - /// - foregroundStyle: The foreground style to apply to the - /// annotation content's environment. - /// - content: A view builder that creates the annotation content. - func annotation( - alignment: HorizontalAlignment = .leading, - spacing: CGFloat? = nil, - font: Font? = .callout, - foregroundStyle: S = .secondary, - @ViewBuilder content: () -> Content - ) -> some View { - VStack(alignment: alignment, spacing: spacing) { - self - content() - .font(font) - .foregroundStyle(foregroundStyle) - } - } -} diff --git a/Ice/UI/ViewModifiers/Once.swift b/Ice/UI/ViewModifiers/Once.swift new file mode 100644 index 00000000..d7d116e9 --- /dev/null +++ b/Ice/UI/ViewModifiers/Once.swift @@ -0,0 +1,31 @@ +// +// Once.swift +// Ice +// + +import SwiftUI + +private struct OnceModifier: ViewModifier { + @State private var hasAppeared = false + + let onAppear: () -> Void + + func body(content: Content) -> some View { + content.onAppear { + if !hasAppeared { + onAppear() + hasAppeared = true + } + } + } +} + +extension View { + /// Adds an action to perform exactly once, before the first + /// time the view appears. + /// + /// - Parameter action: The action to perform. + func once(perform action: @escaping () -> Void) -> some View { + modifier(OnceModifier(onAppear: action)) + } +} diff --git a/Ice/UI/Views/AnnotationView.swift b/Ice/UI/Views/AnnotationView.swift new file mode 100644 index 00000000..54aaeae2 --- /dev/null +++ b/Ice/UI/Views/AnnotationView.swift @@ -0,0 +1,188 @@ +// +// AnnotationView.swift +// Ice +// + +import SwiftUI + +/// A view that displays content as an annotation below a parent view. +struct AnnotationView: View { + private let alignment: HorizontalAlignment + private let spacing: CGFloat + private let font: Font? + private let foregroundStyle: ForegroundStyle + private let parent: Parent + private let content: Content + + /// Creates an annotation view with a parent and content view. + /// + /// - Parameters: + /// - alignment: The alignment of the content view in relation to the parent view. + /// - spacing: The spacing between the parent and content view. + /// - font: The font to apply to the content view's environment. + /// - foregroundStyle: The foreground style to apply to the content view's environment. + /// - parent: The parent view of the annotation. + /// - content: The content view of the annotation. + init( + alignment: HorizontalAlignment = .leading, + spacing: CGFloat = 0, + font: Font? = .subheadline, + foregroundStyle: ForegroundStyle = .secondary, + @ViewBuilder parent: () -> Parent, + @ViewBuilder content: () -> Content + ) { + self.alignment = alignment + self.spacing = spacing + self.font = font + self.foregroundStyle = foregroundStyle + self.parent = parent() + self.content = content() + } + + /// Creates an annotation view with a string key and parent view. + /// + /// - Parameters: + /// - titleKey: The string key to display as text below the parent view. + /// - alignment: The alignment of the content view in relation to the parent view. + /// - spacing: The spacing between the parent and content view. + /// - font: The font to apply to the content view's environment. + /// - foregroundStyle: The foreground style to apply to the content view's environment. + /// - parent: The parent view of the annotation. + init( + _ titleKey: LocalizedStringKey, + alignment: HorizontalAlignment = .leading, + spacing: CGFloat = 0, + font: Font? = .subheadline, + foregroundStyle: ForegroundStyle = .secondary, + @ViewBuilder parent: () -> Parent + ) where Content == Text { + self.init( + alignment: alignment, + spacing: spacing, + font: font, + foregroundStyle: foregroundStyle + ) { + parent() + } content: { + Text(titleKey) + } + } + + /// Creates an annotation view with a content view. + /// + /// - Parameters: + /// - alignment: The alignment of the content view in relation to the parent view. + /// - spacing: The spacing between the parent and content view. + /// - font: The font to apply to the content view's environment. + /// - foregroundStyle: The foreground style to apply to the content view's environment. + /// - content: The content view of the annotation. + init( + alignment: HorizontalAlignment = .leading, + spacing: CGFloat = 0, + font: Font? = .subheadline, + foregroundStyle: ForegroundStyle = .secondary, + @ViewBuilder content: () -> Content + ) where Parent == EmptyView { + self.init( + alignment: alignment, + spacing: spacing, + font: font, + foregroundStyle: foregroundStyle + ) { + EmptyView() + } content: { + content() + } + } + + /// Creates an annotation view with a string key. + /// + /// - Parameters: + /// - titleKey: The string key to display as text. + /// - alignment: The alignment of the content view in relation to the parent view. + /// - spacing: The spacing between the parent and content view. + /// - font: The font to apply to the content view's environment. + /// - foregroundStyle: The foreground style to apply to the content view's environment. + init( + _ titleKey: LocalizedStringKey, + alignment: HorizontalAlignment = .leading, + spacing: CGFloat = 0, + font: Font? = .subheadline, + foregroundStyle: ForegroundStyle = .secondary + ) where Parent == EmptyView, Content == Text { + self.init( + titleKey, + alignment: alignment, + spacing: spacing, + font: font, + foregroundStyle: foregroundStyle + ) { + EmptyView() + } + } + + var body: some View { + VStack(alignment: alignment, spacing: spacing) { + parent + content + .font(font) + .foregroundStyle(foregroundStyle) + } + .frame(maxWidth: .infinity, alignment: Alignment(horizontal: alignment, vertical: .center)) + } +} + +extension View { + /// Adds the given view as an annotation below this view. + /// + /// - Parameters: + /// - alignment: The guide for aligning the annotation content horizontally with this view. + /// - spacing: The vertical spacing between this view and the annotation content. + /// - font: The font to apply to the annotation content's environment. + /// - foregroundStyle: The foreground style to apply to the annotation content's environment. + /// - content: A view builder that creates the annotation content. + func annotation( + alignment: HorizontalAlignment = .leading, + spacing: CGFloat = 0, + font: Font? = .subheadline, + foregroundStyle: ForegroundStyle = .secondary, + @ViewBuilder content: () -> Content + ) -> some View { + AnnotationView( + alignment: alignment, + spacing: spacing, + font: font, + foregroundStyle: foregroundStyle + ) { + self + } content: { + content() + } + } + + /// Adds the given text as an annotation below this view. + /// + /// - Parameters: + /// - titleKey: The string key to display as text below this view. + /// - alignment: The guide for aligning the annotation content horizontally with this view. + /// - spacing: The vertical spacing between this view and the annotation content. + /// - font: The font to apply to the annotation content's environment. + /// - foregroundStyle: The foreground style to apply to the annotation content's environment. + func annotation( + _ titleKey: LocalizedStringKey, + alignment: HorizontalAlignment = .leading, + spacing: CGFloat = 0, + font: Font? = .subheadline, + foregroundStyle: ForegroundStyle = .secondary + ) -> some View { + AnnotationView( + titleKey, + alignment: alignment, + spacing: spacing, + font: font, + foregroundStyle: foregroundStyle + ) { + self + } + } +} diff --git a/Ice/UI/Views/BetaBadge.swift b/Ice/UI/Views/BetaBadge.swift new file mode 100644 index 00000000..93e19c01 --- /dev/null +++ b/Ice/UI/Views/BetaBadge.swift @@ -0,0 +1,19 @@ +// +// BetaBadge.swift +// Ice +// + +import SwiftUI + +struct BetaBadge: View { + var body: some View { + Text("BETA") + .foregroundStyle(.green) + .font(.caption) + .padding(.horizontal, 6) + .background { + Capsule(style: .circular) + .fill(.tertiary) + } + } +} diff --git a/Ice/UI/Views/SectionedList.swift b/Ice/UI/Views/SectionedList.swift new file mode 100644 index 00000000..ba7dfae9 --- /dev/null +++ b/Ice/UI/Views/SectionedList.swift @@ -0,0 +1,223 @@ +// +// SectionedList.swift +// Ice +// + +import SwiftUI + +// MARK: - SectionedList + +/// A scrollable list of items broken up by section. +struct SectionedList: View { + private enum ScrollDirection { + case up, down + } + + @Binding var selection: ItemID? + + @Binding var items: [SectionedListItem] + + @State private var itemFrames = [ItemID: CGRect]() + + @State private var scrollIndicatorsFlashTrigger = 0 + + let spacing: CGFloat + + private(set) var contentPadding = EdgeInsets() + + private var nextSelectableItem: SectionedListItem? { + guard + let index = items.firstIndex(where: { $0.id == selection }), + items.indices.contains(index + 1) + else { + return nil + } + return items[(index + 1)...].first { $0.isSelectable } + } + + private var previousSelectableItem: SectionedListItem? { + guard + let index = items.firstIndex(where: { $0.id == selection }), + items.indices.contains(index - 1) + else { + return nil + } + return items[...(index - 1)].last { $0.isSelectable } + } + + /// Creates a sectioned list with the given selection, spacing, and items. + init(selection: Binding, items: Binding<[SectionedListItem]>, spacing: CGFloat = 0) { + self._selection = selection + self._items = items + self.spacing = spacing + } + + var body: some View { + if #available(macOS 15.0, *) { + scrollView + .contentMargins(.all, contentPadding, for: .scrollContent) + .contentMargins(.all, -0.5, for: .scrollIndicators) + } else { + scrollView + .contentMargins(.all, contentPadding, for: .scrollContent) + .contentMargins(.all, -contentPadding, for: .scrollIndicators) + } + } + + @ViewBuilder + private var scrollView: some View { + ScrollViewReader { scrollView in + GeometryReader { geometry in + ScrollView { + scrollContent(scrollView: scrollView, geometry: geometry) + } + } + } + .scrollIndicatorsFlash(trigger: scrollIndicatorsFlashTrigger) + .onKeyDown(key: .downArrow) { + DispatchQueue.main.async { + if let nextSelectableItem { + selection = nextSelectableItem.id + } + } + } + .onKeyDown(key: .upArrow) { + DispatchQueue.main.async { + if let previousSelectableItem { + selection = previousSelectableItem.id + } + } + } + .onKeyDown(key: .return) { + DispatchQueue.main.async { + items.first { $0.id == selection }?.action?() + } + } + .task { + scrollIndicatorsFlashTrigger += 1 + } + } + + @ViewBuilder + private func scrollContent(scrollView: ScrollViewProxy, geometry: GeometryProxy) -> some View { + VStack(spacing: spacing) { + ForEach(items, id: \.id) { item in + SectionedListItemView( + selection: $selection, + itemFrames: $itemFrames, + item: item + ) + .id(item.id) + } + } + .onChange(of: selection) { + guard + let selection, + let direction = scrollDirection(for: selection, geometry: geometry) + else { + return + } + let anchor: UnitPoint = switch direction { + case .up: .top + case .down: .bottom + } + scrollView.scrollTo(selection, anchor: anchor) + } + } + + private func scrollDirection(for selection: ItemID, geometry: GeometryProxy) -> ScrollDirection? { + guard let selectionFrame = itemFrames[selection] else { + return nil + } + let geometryFrame = geometry.frame(in: .global) + if selectionFrame.minY <= geometryFrame.minY + contentPadding.top { + return .up + } + if selectionFrame.maxY >= geometryFrame.maxY - contentPadding.bottom { + return .down + } + return nil + } +} + +// MARK: SectionedList Content Padding +extension SectionedList { + /// Sets the padding of the sectioned list's content. + func contentPadding(_ insets: EdgeInsets) -> SectionedList { + with(self) { copy in + copy.contentPadding = insets + } + } + + /// Sets the padding of the sectioned list's content. + func contentPadding(_ length: CGFloat) -> SectionedList { + contentPadding(EdgeInsets(top: length, leading: length, bottom: length, trailing: length)) + } +} + +// MARK: - SectionedListItem + +/// An item in a sectioned list. +struct SectionedListItem { + let content: AnyView + let id: ID + let isSelectable: Bool + let action: (() -> Void)? + + /// Returns a selectable item for a sectioned list. + static func item(id: ID, isSelectable: Bool = true, action: (() -> Void)? = nil, @ViewBuilder content: () -> some View) -> SectionedListItem { + SectionedListItem(content: AnyView(content()), id: id, isSelectable: isSelectable, action: action) + } + + /// Returns a section header item for a sectioned list. + static func header(id: ID, @ViewBuilder content: () -> some View) -> SectionedListItem { + item(id: id, isSelectable: false, action: nil) { + content() + } + } +} + +// MARK: - SectionedListItemView + +private struct SectionedListItemView: View { + @Binding var selection: ItemID? + @Binding var itemFrames: [ItemID: CGRect] + @State private var isHovering = false + + let item: SectionedListItem + + var body: some View { + ZStack { + if item.isSelectable { + if selection == item.id { + itemBackground.opacity(0.5) + } else if isHovering { + itemBackground.opacity(0.25) + } + } + item.content + } + .frame(minWidth: 22, minHeight: 22) + .contentShape(Rectangle()) + .onHover { hovering in + isHovering = hovering + } + .onTapGesture { + selection = item.id + } + .simultaneousGesture( + TapGesture(count: 2).onEnded { + item.action?() + } + ) + .onFrameChange(in: .global) { frame in + itemFrames[item.id] = frame + } + } + + @ViewBuilder + private var itemBackground: some View { + VisualEffectView(material: .selection, blendingMode: .withinWindow) + .clipShape(RoundedRectangle(cornerRadius: 5, style: .circular)) + } +} diff --git a/Ice/UI/Views/VisualEffectView.swift b/Ice/UI/Views/VisualEffectView.swift new file mode 100644 index 00000000..d2245230 --- /dev/null +++ b/Ice/UI/Views/VisualEffectView.swift @@ -0,0 +1,26 @@ +// +// VisualEffectView.swift +// Ice +// + +import SwiftUI + +/// A SwiftUI view that wraps an `NSVisualEffectView`. +struct VisualEffectView: NSViewRepresentable { + let material: NSVisualEffectView.Material + let blendingMode: NSVisualEffectView.BlendingMode + + func makeNSView(context: Context) -> NSVisualEffectView { + let visualEffectView = NSVisualEffectView() + visualEffectView.material = material + visualEffectView.blendingMode = blendingMode + visualEffectView.state = .active + visualEffectView.isEmphasized = true + return visualEffectView + } + + func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) { + visualEffectView.material = material + visualEffectView.blendingMode = blendingMode + } +} diff --git a/Ice/Utilities/Constants.swift b/Ice/Utilities/Constants.swift index a13225a0..f964dea7 100644 --- a/Ice/Utilities/Constants.swift +++ b/Ice/Utilities/Constants.swift @@ -22,4 +22,10 @@ enum Constants { /// The identifier for the permissions window. static let permissionsWindowID = "PermissionsWindow" + + /// The title for the settings window. + static let settingsWindowTitle = "Ice" + + /// The title for the permissions window. + static let permissionsWindowTitle = "Permissions" } diff --git a/Ice/Utilities/Defaults.swift b/Ice/Utilities/Defaults.swift index c7b87a40..2cc91250 100644 --- a/Ice/Utilities/Defaults.swift +++ b/Ice/Utilities/Defaults.swift @@ -146,8 +146,8 @@ extension Defaults { case useIceBar = "UseIceBar" case showOnClick = "ShowOnClick" case showOnHover = "ShowOnHover" - case showOnHoverDelay = "ShowOnHoverDelay" case showOnScroll = "ShowOnScroll" + case itemSpacingOffset = "ItemSpacingOffset" case autoRehide = "AutoRehide" case rehideStrategy = "RehideStrategy" case rehideInterval = "RehideInterval" @@ -162,6 +162,8 @@ extension Defaults { case showSectionDividers = "ShowSectionDividers" case enableAlwaysHiddenSection = "EnableAlwaysHiddenSection" case canToggleAlwaysHiddenSection = "CanToggleAlwaysHiddenSection" + case showOnHoverDelay = "ShowOnHoverDelay" + case tempShowInterval = "TempShowInterval" // MARK: Menu Bar Appearance Settings