diff --git a/.gitignore b/.gitignore index 7609e7ea..c505c0ff 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ Loop.app *.mode2v3 *.perspectivev3 *.xcuserstate -project.xcworkspace xcuserdata Loop.zip -Build/ +Build/ \ No newline at end of file diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 18d463c9..8d145373 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -3,32 +3,32 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ - 0A6DC3EB2BB869DE002AB05F /* WindowDirection+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6DC3EA2BB869DE002AB05F /* WindowDirection+Image.swift */; }; + 0A6DC3EB2BB869DE002AB05F /* WindowAction+Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A6DC3EA2BB869DE002AB05F /* WindowAction+Image.swift */; }; 0AFE802E2BB98E81009CF06F /* WindowDirection+LocalizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AFE802D2BB98E81009CF06F /* WindowDirection+LocalizedString.swift */; }; - 116CE7302B9F21520014C19D /* ExcludeListSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116CE72F2B9F21520014C19D /* ExcludeListSettingsView.swift */; }; - 116CE7322B9F24D80014C19D /* AppListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 116CE7312B9F24D80014C19D /* AppListManager.swift */; }; - 117C89682BA4CD6F00A32CE7 /* NSImage+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 117C89672BA4CD6F00A32CE7 /* NSImage+Extensions.swift */; }; 284A37192BB8F16800FC9465 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 284A37182BB8F16800FC9465 /* Localizable.xcstrings */; }; A80397D22A93287C006D2796 /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = A80397D12A93287C006D2796 /* MenuBarExtraAccess */; }; A80397D42A932993006D2796 /* MenuBarIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80397D32A932993006D2796 /* MenuBarIconView.swift */; }; A8055EC22AFEDE0B00459D13 /* Keycorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8055EC12AFEDE0B00459D13 /* Keycorder.swift */; }; - A8063A6E2B19599D00EAB3D9 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8063A6D2B19599D00EAB3D9 /* SettingsView.swift */; }; - A8063A712B195C9100EAB3D9 /* SettingsAccess in Frameworks */ = {isa = PBXBuildFile; productRef = A8063A702B195C9100EAB3D9 /* SettingsAccess */; }; A8063A732B19891900EAB3D9 /* grid.metal in Sources */ = {isa = PBXBuildFile; fileRef = A8063A722B19891900EAB3D9 /* grid.metal */; }; - A80900D42AA3F9F30085C63B /* UnstableIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80900D22AA3F9F20085C63B /* UnstableIndicator.swift */; }; A80900D52AA3F9F30085C63B /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80900D32AA3F9F20085C63B /* VisualEffectView.swift */; }; A80D49BB2BAE479900493B67 /* WindowAction+Port.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80D49BA2BAE479900493B67 /* WindowAction+Port.swift */; }; A81989062AC8EDB300EFF7A1 /* MenuBarHeaderText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81989052AC8EDB300EFF7A1 /* MenuBarHeaderText.swift */; }; A81989082AC8F2AE00EFF7A1 /* MenuBarResizeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81989072AC8F2AE00EFF7A1 /* MenuBarResizeButton.swift */; }; - A82436062B7EE55C0052FBFB /* CrispValueAdjuster.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82436052B7EE55C0052FBFB /* CrispValueAdjuster.swift */; }; + A81B98182BDC854F005FD78C /* AboutConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81B98172BDC854F005FD78C /* AboutConfiguration.swift */; }; + A81D8D0A2C068B8700188E12 /* LuminarePreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81D8D092C068B8700188E12 /* LuminarePreviewView.swift */; }; + A81D8D0C2C06950000188E12 /* LuminareManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A81D8D0B2C06950000188E12 /* LuminareManager.swift */; }; A82521EC29E234EB00139654 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82521EB29E234EB00139654 /* AboutViewController.swift */; }; A82521EE29E235AC00139654 /* PermissionsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82521ED29E235AC00139654 /* PermissionsManager.swift */; }; A82740982AB00FCE00B9BDC5 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82740972AB00FCE00B9BDC5 /* Color+Extensions.swift */; }; A827409A2AB0208500B9BDC5 /* TriggerKeycorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82740992AB0208500B9BDC5 /* TriggerKeycorder.swift */; }; + A82B1AEE2BD352A100E2F3F9 /* AccentColorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82B1AED2BD352A100E2F3F9 /* AccentColorConfiguration.swift */; }; + A82B1AF02BD357FC00E2F3F9 /* RadialMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82B1AEF2BD357FC00E2F3F9 /* RadialMenuConfiguration.swift */; }; + A82B1AF22BD35A3800E2F3F9 /* PreviewConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82B1AF12BD35A3800E2F3F9 /* PreviewConfiguration.swift */; }; + A82B1AF62BD35C8500E2F3F9 /* BehaviorConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82B1AF52BD35C8500E2F3F9 /* BehaviorConfiguration.swift */; }; A82DDBDE2AEC736300D7F974 /* AnimationConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A82DDBDD2AEC736300D7F974 /* AnimationConfiguration.swift */; }; A8330ABD2A3AC0CA00673C8D /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330ABC2A3AC0CA00673C8D /* Bundle+Extensions.swift */; }; A8330AC12A3AC13100673C8D /* Defaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330AC02A3AC13100673C8D /* Defaults+Extensions.swift */; }; @@ -37,28 +37,22 @@ A8330ACB2A3AC1C000673C8D /* Angle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330ACA2A3AC1C000673C8D /* Angle+Extensions.swift */; }; A8330ACD2A3AC1D100673C8D /* CGGeometry+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330ACC2A3AC1D100673C8D /* CGGeometry+Extensions.swift */; }; A8330ACF2A3AC1E900673C8D /* View+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330ACE2A3AC1E900673C8D /* View+Extensions.swift */; }; - A8330AD12A3AC25100673C8D /* CaseIterable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330AD02A3AC25100673C8D /* CaseIterable+Extensions.swift */; }; A8330AD42A3AC27600673C8D /* WindowDirection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8330AD32A3AC27600673C8D /* WindowDirection.swift */; }; A83667C82A3D7D910001D630 /* AXUIElement+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83667C72A3D7D910001D630 /* AXUIElement+Extensions.swift */; }; A83E1C352ABFCA3200853FE9 /* WindowRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = A83E1C342ABFCA3200853FE9 /* WindowRecords.swift */; }; - A84497C92B393595003D4CF3 /* CustomKeybindView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84497C82B393595003D4CF3 /* CustomKeybindView.swift */; }; - A84497CB2B395D8C003D4CF3 /* AnchorPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84497CA2B395D8C003D4CF3 /* AnchorPicker.swift */; }; - A84497CD2B3A52C7003D4CF3 /* PreviewWindowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84497CC2B3A52C7003D4CF3 /* PreviewWindowButton.swift */; }; - A84497D22B3B88A6003D4CF3 /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84497D12B3B88A6003D4CF3 /* Optional+Extensions.swift */; }; + A8427E662C02594E00F20759 /* ExcludedAppsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8427E652C02594E00F20759 /* ExcludedAppsConfiguration.swift */; }; A848D8A72A8C2F3F00060834 /* LoopManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A848D8A62A8C2F3F00060834 /* LoopManager.swift */; }; - A84F9E292BA4F20C00CF3E09 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = A84F9E282BA4F20C00CF3E09 /* Algorithms */; }; A8504D2D2A85832F00C2EFDA /* SoftwareUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */; }; + A858914B2BDC5D3F00C10FB1 /* AdvancedConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A858914A2BDC5D3F00C10FB1 /* AdvancedConfiguration.swift */; }; A859799B2B55FE94009FB067 /* UNNotification+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A859799A2B55FE94009FB067 /* UNNotification+Extensions.swift */; }; A85B560E2AAAD62C00386ACE /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85B560D2AAAD62C00386ACE /* EventMonitor.swift */; }; A85CB5852ACFA5F700BF63E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */; }; - A85FEEBC2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85FEEBB2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift */; }; + A85DDBDA2C1693D4008C103D /* WindowDirection+Snapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85DDBD92C1693D4008C103D /* WindowDirection+Snapping.swift */; }; A864F4682AA660CD00579738 /* WindowDragManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A864F4672AA660CD00579738 /* WindowDragManager.swift */; }; A86949862A8F2BB70051AAAF /* CGKeyCode+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */; }; A869C1A12B38C6E600AD1A84 /* StageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A869C1A02B38C6E600AD1A84 /* StageManager.swift */; }; A86B97AD2AB79E2500099D7F /* ShakeEffect.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */; }; A86CB7332A3D22E7006A78F2 /* WindowEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */; }; - A86DAE2A2B3CCF2900B968F0 /* CustomCyclingKeybindView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86DAE292B3CCF2900B968F0 /* CustomCyclingKeybindView.swift */; }; - A86DAE2C2B3E31F800B968F0 /* CustomCyclingKeybindItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A86DAE2B2B3E31F800B968F0 /* CustomCyclingKeybindItemView.swift */; }; A86DDA022BA4F6E900C0DFF7 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A86DDA012BA4F6E900C0DFF7 /* Sparkle */; }; A87376F62AA288EB001890F4 /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = A87376F52AA288EB001890F4 /* Window.swift */; }; A8789F6729805B190040512E /* RadialMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8789F6629805B190040512E /* RadialMenuView.swift */; }; @@ -69,20 +63,25 @@ A882660829809F6F00BCB197 /* GeneralSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A882660729809F6F00BCB197 /* GeneralSettingsView.swift */; }; A883642F298B7288005D6C19 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A883642E298B7288005D6C19 /* ServiceManagement.framework */; }; A8878A252AA3B2C800850A66 /* WindowTransformAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8878A242AA3B2C800850A66 /* WindowTransformAnimation.swift */; }; + A88E27AD2BDDE5300042CF04 /* CustomActionConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88E27AC2BDDE5300042CF04 /* CustomActionConfigurationView.swift */; }; A88E83C52B37B354009D332F /* CGEvent+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88E83C42B37B354009D332F /* CGEvent+Extensions.swift */; }; A89307312BAE6D0100566AEE /* CustomWindowActionUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A89307302BAE6D0100566AEE /* CustomWindowActionUnit.swift */; }; + A893D3642BD3299000063510 /* IconConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A893D3632BD3299000063510 /* IconConfiguration.swift */; }; + A8A1C51E2BD3705A00515A14 /* PaddingConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A1C51D2BD3705A00515A14 /* PaddingConfigurationView.swift */; }; + A8A1C5212BD4863B00515A14 /* KeybindingsConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A1C5202BD4863B00515A14 /* KeybindingsConfiguration.swift */; }; A8A2ABE72A3FB0370067B5A9 /* KeybindMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A2ABE62A3FB0370067B5A9 /* KeybindMonitor.swift */; }; + A8A583B82BE5A117005F4CB2 /* CycleActionConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A583B72BE5A117005F4CB2 /* CycleActionConfigurationView.swift */; }; + A8A583BA2BE5A8D8005F4CB2 /* KeybindingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8A583B92BE5A8D8005F4CB2 /* KeybindingItem.swift */; }; A8B5E1632B43726C00044D30 /* CustomWindowActionAnchor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8B5E1622B43726C00044D30 /* CustomWindowActionAnchor.swift */; }; - A8BE09DB2B113FD700DBB242 /* KeycorderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BE09DA2B113FD700DBB242 /* KeycorderModel.swift */; }; + A8BC77792C0EB4DD008E2EDA /* AppDelegate+UNNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8BC77782C0EB4DD008E2EDA /* AppDelegate+UNNotifications.swift */; }; + A8C83C6B2C0D1D560024A7A1 /* Luminare in Frameworks */ = {isa = PBXBuildFile; productRef = A8C83C6A2C0D1D560024A7A1 /* Luminare */; }; + A8D4327B2C13ED3C007BE4F2 /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D4327A2C13ED3C007BE4F2 /* Icon.swift */; }; A8D5A7D62A91384D004EA5BB /* DirectionSelectorSquareSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D5A7D52A91384D004EA5BB /* DirectionSelectorSquareSegment.swift */; }; A8D5A7D82A913862004EA5BB /* DirectionSelectorCircleSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D5A7D72A913862004EA5BB /* DirectionSelectorCircleSegment.swift */; }; - A8D6D2FF2B6C87F80061B11F /* PaddingConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D6D2FE2B6C87F80061B11F /* PaddingConfigurationView.swift */; }; A8D6D3012B6C894C0061B11F /* PaddingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D6D3002B6C894C0061B11F /* PaddingModel.swift */; }; A8D6D3032B6C8D750061B11F /* PaddingPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D6D3022B6C8D750061B11F /* PaddingPreviewView.swift */; }; A8D6D3052B6C92F20061B11F /* WallpaperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8D6D3042B6C92F20061B11F /* WallpaperView.swift */; }; A8DCC97B2980D5F500D41065 /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = A8DCC97A2980D5F500D41065 /* Defaults */; }; - A8DCC9882981B9E100D41065 /* RadialMenuSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8DCC9872981B9E100D41065 /* RadialMenuSettingsView.swift */; }; - A8DCC98A2981F43F00D41065 /* PreviewSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8DCC9892981F43F00D41065 /* PreviewSettingsView.swift */; }; A8E1575F298654960005761C /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E1575E298654960005761C /* AboutView.swift */; }; A8E59C39297F5E9A0064D4BA /* LoopApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E59C38297F5E9A0064D4BA /* LoopApp.swift */; }; A8E59C3D297F5E9B0064D4BA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A8E59C3C297F5E9B0064D4BA /* Assets.xcassets */; }; @@ -92,33 +91,31 @@ A8E8903C2BA7AAFE006C5074 /* NSEvent+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8E8903B2BA7AAFE006C5074 /* NSEvent+Extensions.swift */; }; A8EF1F09299C87DF00633440 /* IconManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8EF1F08299C87DF00633440 /* IconManager.swift */; }; A8F0125B2AEDD7660017307F /* WindowAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8F0125A2AEDD7660017307F /* WindowAction.swift */; }; - A8F0125D2AEDFEC70017307F /* KeybindingsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8F0125C2AEDFEC70017307F /* KeybindingsSettingsView.swift */; }; - A8F0636D2985AF1F0010C33D /* MoreSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8F0636C2985AF1F0010C33D /* MoreSettingsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 0A6DC3EA2BB869DE002AB05F /* WindowDirection+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowDirection+Image.swift"; sourceTree = ""; }; + 0A6DC3EA2BB869DE002AB05F /* WindowAction+Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowAction+Image.swift"; sourceTree = ""; }; 0AFE802D2BB98E81009CF06F /* WindowDirection+LocalizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowDirection+LocalizedString.swift"; sourceTree = ""; }; - 116CE72F2B9F21520014C19D /* ExcludeListSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcludeListSettingsView.swift; sourceTree = ""; }; - 116CE7312B9F24D80014C19D /* AppListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppListManager.swift; sourceTree = ""; }; - 117C89672BA4CD6F00A32CE7 /* NSImage+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Extensions.swift"; sourceTree = ""; }; 284A37182BB8F16800FC9465 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; A80397D32A932993006D2796 /* MenuBarIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarIconView.swift; sourceTree = ""; }; A80521312A84878200BF7E22 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; A8055EC12AFEDE0B00459D13 /* Keycorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keycorder.swift; sourceTree = ""; }; - A8063A6D2B19599D00EAB3D9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A8063A722B19891900EAB3D9 /* grid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = grid.metal; sourceTree = ""; }; - A80900D22AA3F9F20085C63B /* UnstableIndicator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnstableIndicator.swift; sourceTree = ""; }; A80900D32AA3F9F20085C63B /* VisualEffectView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; A80D49BA2BAE479900493B67 /* WindowAction+Port.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowAction+Port.swift"; sourceTree = ""; }; A81989052AC8EDB300EFF7A1 /* MenuBarHeaderText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarHeaderText.swift; sourceTree = ""; }; A81989072AC8F2AE00EFF7A1 /* MenuBarResizeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuBarResizeButton.swift; sourceTree = ""; }; - A82436052B7EE55C0052FBFB /* CrispValueAdjuster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrispValueAdjuster.swift; sourceTree = ""; }; + A81B98172BDC854F005FD78C /* AboutConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutConfiguration.swift; sourceTree = ""; }; + A81D8D092C068B8700188E12 /* LuminarePreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuminarePreviewView.swift; sourceTree = ""; }; + A81D8D0B2C06950000188E12 /* LuminareManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LuminareManager.swift; sourceTree = ""; }; A82521EB29E234EB00139654 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; A82521ED29E235AC00139654 /* PermissionsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionsManager.swift; sourceTree = ""; }; A82740972AB00FCE00B9BDC5 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; A82740992AB0208500B9BDC5 /* TriggerKeycorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggerKeycorder.swift; sourceTree = ""; }; - A8291D6D2A4513D200C5CB69 /* .swiftlint.yml */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; tabWidth = 2; }; + A82B1AED2BD352A100E2F3F9 /* AccentColorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccentColorConfiguration.swift; sourceTree = ""; }; + A82B1AEF2BD357FC00E2F3F9 /* RadialMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialMenuConfiguration.swift; sourceTree = ""; }; + A82B1AF12BD35A3800E2F3F9 /* PreviewConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewConfiguration.swift; sourceTree = ""; }; + A82B1AF52BD35C8500E2F3F9 /* BehaviorConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorConfiguration.swift; sourceTree = ""; }; A82DDBDD2AEC736300D7F974 /* AnimationConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationConfiguration.swift; sourceTree = ""; }; A8330ABC2A3AC0CA00673C8D /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = ""; }; A8330AC02A3AC13100673C8D /* Defaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Defaults+Extensions.swift"; sourceTree = ""; }; @@ -127,29 +124,24 @@ A8330ACA2A3AC1C000673C8D /* Angle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Angle+Extensions.swift"; sourceTree = ""; }; A8330ACC2A3AC1D100673C8D /* CGGeometry+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGGeometry+Extensions.swift"; sourceTree = ""; }; A8330ACE2A3AC1E900673C8D /* View+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Extensions.swift"; sourceTree = ""; }; - A8330AD02A3AC25100673C8D /* CaseIterable+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CaseIterable+Extensions.swift"; sourceTree = ""; }; A8330AD32A3AC27600673C8D /* WindowDirection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDirection.swift; sourceTree = ""; }; A83667C72A3D7D910001D630 /* AXUIElement+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AXUIElement+Extensions.swift"; sourceTree = ""; }; A83E1C342ABFCA3200853FE9 /* WindowRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowRecords.swift; sourceTree = ""; }; - A84497C82B393595003D4CF3 /* CustomKeybindView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomKeybindView.swift; sourceTree = ""; }; - A84497CA2B395D8C003D4CF3 /* AnchorPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnchorPicker.swift; sourceTree = ""; }; - A84497CC2B3A52C7003D4CF3 /* PreviewWindowButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewWindowButton.swift; sourceTree = ""; }; - A84497D12B3B88A6003D4CF3 /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = ""; }; + A8427E652C02594E00F20759 /* ExcludedAppsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcludedAppsConfiguration.swift; sourceTree = ""; }; A848130C2BD1A8D100B02E93 /* .github */ = {isa = PBXFileReference; lastKnownFileType = folder; path = .github; sourceTree = ""; }; A848D8A62A8C2F3F00060834 /* LoopManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopManager.swift; sourceTree = ""; }; A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdater.swift; sourceTree = ""; }; + A858914A2BDC5D3F00C10FB1 /* AdvancedConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedConfiguration.swift; sourceTree = ""; }; A859799A2B55FE94009FB067 /* UNNotification+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Extensions.swift"; sourceTree = ""; }; A85B560D2AAAD62C00386ACE /* EventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = ""; }; A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - A85FEEBB2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindCustomizationViewItem.swift; sourceTree = ""; }; + A85DDBD92C1693D4008C103D /* WindowDirection+Snapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WindowDirection+Snapping.swift"; sourceTree = ""; }; A864F4672AA660CD00579738 /* WindowDragManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowDragManager.swift; sourceTree = ""; }; A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CGKeyCode+Extensions.swift"; sourceTree = ""; }; A869C1A02B38C6E600AD1A84 /* StageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageManager.swift; sourceTree = ""; }; A86AFD7529888B29008F4892 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShakeEffect.swift; sourceTree = ""; }; A86CB7322A3D22E7006A78F2 /* WindowEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowEngine.swift; sourceTree = ""; }; - A86DAE292B3CCF2900B968F0 /* CustomCyclingKeybindView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCyclingKeybindView.swift; sourceTree = ""; }; - A86DAE2B2B3E31F800B968F0 /* CustomCyclingKeybindItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCyclingKeybindItemView.swift; sourceTree = ""; }; A87376F52AA288EB001890F4 /* Window.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Window.swift; sourceTree = ""; }; A8789F6629805B190040512E /* RadialMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialMenuView.swift; sourceTree = ""; }; A8789F6829805B340040512E /* PreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewView.swift; sourceTree = ""; }; @@ -159,19 +151,25 @@ A882660729809F6F00BCB197 /* GeneralSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralSettingsView.swift; sourceTree = ""; }; A883642E298B7288005D6C19 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; A8878A242AA3B2C800850A66 /* WindowTransformAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowTransformAnimation.swift; sourceTree = ""; }; + A88E27AC2BDDE5300042CF04 /* CustomActionConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomActionConfigurationView.swift; sourceTree = ""; }; A88E83C42B37B354009D332F /* CGEvent+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGEvent+Extensions.swift"; sourceTree = ""; }; A89307302BAE6D0100566AEE /* CustomWindowActionUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomWindowActionUnit.swift; sourceTree = ""; }; + A893D3632BD3299000063510 /* IconConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconConfiguration.swift; sourceTree = ""; }; + A8A1C51D2BD3705A00515A14 /* PaddingConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingConfigurationView.swift; sourceTree = ""; }; + A8A1C5202BD4863B00515A14 /* KeybindingsConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindingsConfiguration.swift; sourceTree = ""; }; A8A2ABE62A3FB0370067B5A9 /* KeybindMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindMonitor.swift; sourceTree = ""; }; + A8A583B72BE5A117005F4CB2 /* CycleActionConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CycleActionConfigurationView.swift; sourceTree = ""; }; + A8A583B92BE5A8D8005F4CB2 /* KeybindingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindingItem.swift; sourceTree = ""; }; A8B5E1622B43726C00044D30 /* CustomWindowActionAnchor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomWindowActionAnchor.swift; sourceTree = ""; }; - A8BE09DA2B113FD700DBB242 /* KeycorderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeycorderModel.swift; sourceTree = ""; }; + A8BC77782C0EB4DD008E2EDA /* AppDelegate+UNNotifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppDelegate+UNNotifications.swift"; sourceTree = ""; }; + A8C83C672C0D1CDE0024A7A1 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = ../Luminare/Package.swift; sourceTree = ""; }; + A8C83C682C0D1CEA0024A7A1 /* Luminare */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Luminare; path = ../Luminare; sourceTree = ""; }; + A8D4327A2C13ED3C007BE4F2 /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; A8D5A7D52A91384D004EA5BB /* DirectionSelectorSquareSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionSelectorSquareSegment.swift; sourceTree = ""; }; A8D5A7D72A913862004EA5BB /* DirectionSelectorCircleSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionSelectorCircleSegment.swift; sourceTree = ""; }; - A8D6D2FE2B6C87F80061B11F /* PaddingConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingConfigurationView.swift; sourceTree = ""; }; A8D6D3002B6C894C0061B11F /* PaddingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddingModel.swift; sourceTree = ""; }; A8D6D3022B6C8D750061B11F /* PaddingPreviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaddingPreviewView.swift; sourceTree = ""; }; A8D6D3042B6C92F20061B11F /* WallpaperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WallpaperView.swift; sourceTree = ""; }; - A8DCC9872981B9E100D41065 /* RadialMenuSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadialMenuSettingsView.swift; sourceTree = ""; }; - A8DCC9892981F43F00D41065 /* PreviewSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewSettingsView.swift; sourceTree = ""; }; A8E1575E298654960005761C /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; A8E59C35297F5E9A0064D4BA /* Loop.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Loop.app; sourceTree = BUILT_PRODUCTS_DIR; }; A8E59C38297F5E9A0064D4BA /* LoopApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopApp.swift; sourceTree = ""; }; @@ -184,8 +182,6 @@ A8E8903B2BA7AAFE006C5074 /* NSEvent+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSEvent+Extensions.swift"; sourceTree = ""; }; A8EF1F08299C87DF00633440 /* IconManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconManager.swift; sourceTree = ""; }; A8F0125A2AEDD7660017307F /* WindowAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowAction.swift; sourceTree = ""; }; - A8F0125C2AEDFEC70017307F /* KeybindingsSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindingsSettingsView.swift; sourceTree = ""; }; - A8F0636C2985AF1F0010C33D /* MoreSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreSettingsView.swift; sourceTree = ""; }; A8F063712985B2730010C33D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ @@ -194,12 +190,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A84F9E292BA4F20C00CF3E09 /* Algorithms in Frameworks */, A8DCC97B2980D5F500D41065 /* Defaults in Frameworks */, A883642F298B7288005D6C19 /* ServiceManagement.framework in Frameworks */, A86DDA022BA4F6E900C0DFF7 /* Sparkle in Frameworks */, + A8C83C6B2C0D1D560024A7A1 /* Luminare in Frameworks */, A80397D22A93287C006D2796 /* MenuBarExtraAccess in Frameworks */, - A8063A712B195C9100EAB3D9 /* SettingsAccess in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -209,10 +204,9 @@ A80900D12AA3F9F20085C63B /* Utilities */ = { isa = PBXGroup; children = ( - A82436052B7EE55C0052FBFB /* CrispValueAdjuster.swift */, + A8D4327A2C13ED3C007BE4F2 /* Icon.swift */, A82DDBDD2AEC736300D7F974 /* AnimationConfiguration.swift */, A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */, - A80900D22AA3F9F20085C63B /* UnstableIndicator.swift */, A80900D32AA3F9F20085C63B /* VisualEffectView.swift */, A85B560D2AAAD62C00386ACE /* EventMonitor.swift */, A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */, @@ -232,6 +226,26 @@ path = "About Window"; sourceTree = ""; }; + A82B1AF32BD35C5900E2F3F9 /* Theming */ = { + isa = PBXGroup; + children = ( + A893D3632BD3299000063510 /* IconConfiguration.swift */, + A82B1AED2BD352A100E2F3F9 /* AccentColorConfiguration.swift */, + A82B1AEF2BD357FC00E2F3F9 /* RadialMenuConfiguration.swift */, + A82B1AF12BD35A3800E2F3F9 /* PreviewConfiguration.swift */, + ); + path = Theming; + sourceTree = ""; + }; + A82B1AF42BD35C6400E2F3F9 /* Settings */ = { + isa = PBXGroup; + children = ( + A8A1C5232BD4864B00515A14 /* Behavior */, + A8A1C5222BD4863F00515A14 /* Keybindings */, + ); + path = Settings; + sourceTree = ""; + }; A8330ABB2A3AC05200673C8D /* Managers */ = { isa = PBXGroup; children = ( @@ -242,7 +256,6 @@ A82521ED29E235AC00139654 /* PermissionsManager.swift */, A8EF1F08299C87DF00633440 /* IconManager.swift */, A869C1A02B38C6E600AD1A84 /* StageManager.swift */, - 116CE7312B9F24D80014C19D /* AppListManager.swift */, ); path = Managers; sourceTree = ""; @@ -253,17 +266,14 @@ A8330ACA2A3AC1C000673C8D /* Angle+Extensions.swift */, A83667C72A3D7D910001D630 /* AXUIElement+Extensions.swift */, A8330ABC2A3AC0CA00673C8D /* Bundle+Extensions.swift */, - A8330AD02A3AC25100673C8D /* CaseIterable+Extensions.swift */, A88E83C42B37B354009D332F /* CGEvent+Extensions.swift */, A8330ACC2A3AC1D100673C8D /* CGGeometry+Extensions.swift */, A86949852A8F2BB60051AAAF /* CGKeyCode+Extensions.swift */, A82740972AB00FCE00B9BDC5 /* Color+Extensions.swift */, A8330AC02A3AC13100673C8D /* Defaults+Extensions.swift */, A8330AC42A3AC15900673C8D /* Notification+Extensions.swift */, - 117C89672BA4CD6F00A32CE7 /* NSImage+Extensions.swift */, A8E8903B2BA7AAFE006C5074 /* NSEvent+Extensions.swift */, A8330AC62A3AC19500673C8D /* NSScreen+Extensions.swift */, - A84497D12B3B88A6003D4CF3 /* Optional+Extensions.swift */, A859799A2B55FE94009FB067 /* UNNotification+Extensions.swift */, A8330ACE2A3AC1E900673C8D /* View+Extensions.swift */, ); @@ -280,16 +290,14 @@ path = MenuBar; sourceTree = ""; }; - A84497CE2B3A57E8003D4CF3 /* Keybindings */ = { + A85891492BDC5D1600C10FB1 /* Loop */ = { isa = PBXGroup; children = ( - A8F0125C2AEDFEC70017307F /* KeybindingsSettingsView.swift */, - A85FEEBB2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift */, - A86DAE262B3CCEF600B968F0 /* Custom Keybinds */, - A86DAE282B3CCF1600B968F0 /* Custom Cycling Keybinds */, - A86DAE272B3CCF0400B968F0 /* Keybind Recorder */, + A858914A2BDC5D3F00C10FB1 /* AdvancedConfiguration.swift */, + A8427E652C02594E00F20759 /* ExcludedAppsConfiguration.swift */, + A81B98172BDC854F005FD78C /* AboutConfiguration.swift */, ); - path = Keybindings; + path = Loop; sourceTree = ""; }; A864F4662AA65EC200579738 /* Window Management */ = { @@ -300,49 +308,31 @@ A8878A242AA3B2C800850A66 /* WindowTransformAnimation.swift */, A8F0125A2AEDD7660017307F /* WindowAction.swift */, A80D49BA2BAE479900493B67 /* WindowAction+Port.swift */, + 0A6DC3EA2BB869DE002AB05F /* WindowAction+Image.swift */, A8330AD32A3AC27600673C8D /* WindowDirection.swift */, + A85DDBD92C1693D4008C103D /* WindowDirection+Snapping.swift */, 0AFE802D2BB98E81009CF06F /* WindowDirection+LocalizedString.swift */, - 0A6DC3EA2BB869DE002AB05F /* WindowDirection+Image.swift */, A83E1C342ABFCA3200853FE9 /* WindowRecords.swift */, A8B5E1612B43725B00044D30 /* Custom Window Sizes */, ); path = "Window Management"; sourceTree = ""; }; - A86DAE262B3CCEF600B968F0 /* Custom Keybinds */ = { - isa = PBXGroup; - children = ( - A84497C82B393595003D4CF3 /* CustomKeybindView.swift */, - A84497CA2B395D8C003D4CF3 /* AnchorPicker.swift */, - A84497CC2B3A52C7003D4CF3 /* PreviewWindowButton.swift */, - ); - path = "Custom Keybinds"; - sourceTree = ""; - }; A86DAE272B3CCF0400B968F0 /* Keybind Recorder */ = { isa = PBXGroup; children = ( A82740992AB0208500B9BDC5 /* TriggerKeycorder.swift */, A8055EC12AFEDE0B00459D13 /* Keycorder.swift */, - A8BE09DA2B113FD700DBB242 /* KeycorderModel.swift */, ); path = "Keybind Recorder"; sourceTree = ""; }; - A86DAE282B3CCF1600B968F0 /* Custom Cycling Keybinds */ = { - isa = PBXGroup; - children = ( - A86DAE292B3CCF2900B968F0 /* CustomCyclingKeybindView.swift */, - A86DAE2B2B3E31F800B968F0 /* CustomCyclingKeybindItemView.swift */, - ); - path = "Custom Cycling Keybinds"; - sourceTree = ""; - }; A88265FA29808DED00BCB197 /* Preview Window */ = { isa = PBXGroup; children = ( A8E59C4F298045D90064D4BA /* PreviewController.swift */, A8789F6829805B340040512E /* PreviewView.swift */, + A81D8D092C068B8700188E12 /* LuminarePreviewView.swift */, ); path = "Preview Window"; sourceTree = ""; @@ -361,14 +351,7 @@ A88265FC298092B500BCB197 /* Settings */ = { isa = PBXGroup; children = ( - A8063A6D2B19599D00EAB3D9 /* SettingsView.swift */, A882660729809F6F00BCB197 /* GeneralSettingsView.swift */, - A8D6D3062B6C99C10061B11F /* Padding */, - A8DCC9872981B9E100D41065 /* RadialMenuSettingsView.swift */, - A8DCC9892981F43F00D41065 /* PreviewSettingsView.swift */, - A84497CE2B3A57E8003D4CF3 /* Keybindings */, - 116CE72F2B9F21520014C19D /* ExcludeListSettingsView.swift */, - A8F0636C2985AF1F0010C33D /* MoreSettingsView.swift */, ); path = Settings; sourceTree = ""; @@ -376,36 +359,69 @@ A883642D298B7288005D6C19 /* Frameworks */ = { isa = PBXGroup; children = ( + A8C83C682C0D1CEA0024A7A1 /* Luminare */, + A8C83C672C0D1CDE0024A7A1 /* Package.swift */, A883642E298B7288005D6C19 /* ServiceManagement.framework */, ); name = Frameworks; sourceTree = ""; }; - A8B5E1612B43725B00044D30 /* Custom Window Sizes */ = { + A893D3622BD3298700063510 /* Luminare */ = { isa = PBXGroup; children = ( - A87F78952BAE333C0087B1DE /* CustomWindowActionPositionMode.swift */, - A87F78932BAE28050087B1DE /* CustomWindowActionSizeMode.swift */, - A8B5E1622B43726C00044D30 /* CustomWindowActionAnchor.swift */, - A89307302BAE6D0100566AEE /* CustomWindowActionUnit.swift */, + A81D8D0B2C06950000188E12 /* LuminareManager.swift */, + A82B1AF32BD35C5900E2F3F9 /* Theming */, + A82B1AF42BD35C6400E2F3F9 /* Settings */, + A85891492BDC5D1600C10FB1 /* Loop */, ); - path = "Custom Window Sizes"; + path = Luminare; sourceTree = ""; }; - A8D6D3062B6C99C10061B11F /* Padding */ = { + A8A1C51F2BD481AB00515A14 /* Padding Configuration */ = { isa = PBXGroup; children = ( - A8D6D2FE2B6C87F80061B11F /* PaddingConfigurationView.swift */, + A8A1C51D2BD3705A00515A14 /* PaddingConfigurationView.swift */, A8D6D3022B6C8D750061B11F /* PaddingPreviewView.swift */, ); - path = Padding; + path = "Padding Configuration"; + sourceTree = ""; + }; + A8A1C5222BD4863F00515A14 /* Keybindings */ = { + isa = PBXGroup; + children = ( + A8A1C5202BD4863B00515A14 /* KeybindingsConfiguration.swift */, + A8A583B92BE5A8D8005F4CB2 /* KeybindingItem.swift */, + A88E27AC2BDDE5300042CF04 /* CustomActionConfigurationView.swift */, + A8A583B72BE5A117005F4CB2 /* CycleActionConfigurationView.swift */, + A86DAE272B3CCF0400B968F0 /* Keybind Recorder */, + ); + path = Keybindings; + sourceTree = ""; + }; + A8A1C5232BD4864B00515A14 /* Behavior */ = { + isa = PBXGroup; + children = ( + A82B1AF52BD35C8500E2F3F9 /* BehaviorConfiguration.swift */, + A8A1C51F2BD481AB00515A14 /* Padding Configuration */, + ); + path = Behavior; + sourceTree = ""; + }; + A8B5E1612B43725B00044D30 /* Custom Window Sizes */ = { + isa = PBXGroup; + children = ( + A87F78952BAE333C0087B1DE /* CustomWindowActionPositionMode.swift */, + A87F78932BAE28050087B1DE /* CustomWindowActionSizeMode.swift */, + A8B5E1622B43726C00044D30 /* CustomWindowActionAnchor.swift */, + A89307302BAE6D0100566AEE /* CustomWindowActionUnit.swift */, + ); + path = "Custom Window Sizes"; sourceTree = ""; }; A8E59C2C297F5E9A0064D4BA = { isa = PBXGroup; children = ( A848130C2BD1A8D100B02E93 /* .github */, - A8291D6D2A4513D200C5CB69 /* .swiftlint.yml */, A8E6D1FC2A4155DC005751D4 /* .gitignore */, A86AFD7529888B29008F4892 /* README.md */, A8E59C37297F5E9A0064D4BA /* Loop */, @@ -426,8 +442,10 @@ isa = PBXGroup; children = ( A80521312A84878200BF7E22 /* Config.xcconfig */, - A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */, A8E59C38297F5E9A0064D4BA /* LoopApp.swift */, + A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */, + A8BC77782C0EB4DD008E2EDA /* AppDelegate+UNNotifications.swift */, + A893D3622BD3298700063510 /* Luminare */, A83EEEB02AD46BAC00F3EA2D /* MenuBar */, A864F4662AA65EC200579738 /* Window Management */, A8330ABB2A3AC05200673C8D /* Managers */, @@ -461,7 +479,6 @@ isa = PBXNativeTarget; buildConfigurationList = A8E59C44297F5E9B0064D4BA /* Build configuration list for PBXNativeTarget "Loop" */; buildPhases = ( - A8291D6C2A450C2700C5CB69 /* Run SwiftLint */, A8E59C31297F5E9A0064D4BA /* Sources */, A8E59C32297F5E9A0064D4BA /* Frameworks */, A8E59C33297F5E9A0064D4BA /* Resources */, @@ -474,9 +491,8 @@ packageProductDependencies = ( A8DCC97A2980D5F500D41065 /* Defaults */, A80397D12A93287C006D2796 /* MenuBarExtraAccess */, - A8063A702B195C9100EAB3D9 /* SettingsAccess */, - A84F9E282BA4F20C00CF3E09 /* Algorithms */, A86DDA012BA4F6E900C0DFF7 /* Sparkle */, + A8C83C6A2C0D1D560024A7A1 /* Luminare */, ); productName = WindowManager; productReference = A8E59C35297F5E9A0064D4BA /* Loop.app */; @@ -511,8 +527,7 @@ A8DCC9792980D5F500D41065 /* XCRemoteSwiftPackageReference "Defaults" */, A8F0636E2985B2220010C33D /* XCRemoteSwiftPackageReference "Sparkle" */, A80397D02A93287C006D2796 /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */, - A8063A6F2B195C9100EAB3D9 /* XCRemoteSwiftPackageReference "SettingsAccess" */, - 116CE7332B9F31580014C19D /* XCRemoteSwiftPackageReference "swift-algorithms" */, + A8C83C692C0D1D560024A7A1 /* XCLocalSwiftPackageReference "../Luminare" */, ); productRefGroup = A8E59C36297F5E9A0064D4BA /* Products */; projectDirPath = ""; @@ -536,28 +551,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - A8291D6C2A450C2700C5CB69 /* Run SwiftLint */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run SwiftLint"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ A8E59C31297F5E9A0064D4BA /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -565,36 +558,31 @@ files = ( A859799B2B55FE94009FB067 /* UNNotification+Extensions.swift in Sources */, A8EF1F09299C87DF00633440 /* IconManager.swift in Sources */, - A84497D22B3B88A6003D4CF3 /* Optional+Extensions.swift in Sources */, A848D8A72A8C2F3F00060834 /* LoopManager.swift in Sources */, - A8330AD12A3AC25100673C8D /* CaseIterable+Extensions.swift in Sources */, + A82B1AF02BD357FC00E2F3F9 /* RadialMenuConfiguration.swift in Sources */, A8D5A7D62A91384D004EA5BB /* DirectionSelectorSquareSegment.swift in Sources */, - A8BE09DB2B113FD700DBB242 /* KeycorderModel.swift in Sources */, - 116CE7322B9F24D80014C19D /* AppListManager.swift in Sources */, + A8A583BA2BE5A8D8005F4CB2 /* KeybindingItem.swift in Sources */, A82521EC29E234EB00139654 /* AboutViewController.swift in Sources */, A8D6D3012B6C894C0061B11F /* PaddingModel.swift in Sources */, + A88E27AD2BDDE5300042CF04 /* CustomActionConfigurationView.swift in Sources */, A87F78942BAE28050087B1DE /* CustomWindowActionSizeMode.swift in Sources */, - A86DAE2C2B3E31F800B968F0 /* CustomCyclingKeybindItemView.swift in Sources */, A82DDBDE2AEC736300D7F974 /* AnimationConfiguration.swift in Sources */, - A84497CD2B3A52C7003D4CF3 /* PreviewWindowButton.swift in Sources */, A8789F6729805B190040512E /* RadialMenuView.swift in Sources */, A8330ABD2A3AC0CA00673C8D /* Bundle+Extensions.swift in Sources */, A80D49BB2BAE479900493B67 /* WindowAction+Port.swift in Sources */, + A8D4327B2C13ED3C007BE4F2 /* Icon.swift in Sources */, A86B97AD2AB79E2500099D7F /* ShakeEffect.swift in Sources */, A8D6D3032B6C8D750061B11F /* PaddingPreviewView.swift in Sources */, A82740982AB00FCE00B9BDC5 /* Color+Extensions.swift in Sources */, + A82B1AF62BD35C8500E2F3F9 /* BehaviorConfiguration.swift in Sources */, A869C1A12B38C6E600AD1A84 /* StageManager.swift in Sources */, - A8DCC9882981B9E100D41065 /* RadialMenuSettingsView.swift in Sources */, - A82436062B7EE55C0052FBFB /* CrispValueAdjuster.swift in Sources */, - A8F0636D2985AF1F0010C33D /* MoreSettingsView.swift in Sources */, A8330ACD2A3AC1D100673C8D /* CGGeometry+Extensions.swift in Sources */, + A82B1AF22BD35A3800E2F3F9 /* PreviewConfiguration.swift in Sources */, A8330AC72A3AC19500673C8D /* NSScreen+Extensions.swift in Sources */, A80900D52AA3F9F30085C63B /* VisualEffectView.swift in Sources */, A8330AC12A3AC13100673C8D /* Defaults+Extensions.swift in Sources */, - A80900D42AA3F9F30085C63B /* UnstableIndicator.swift in Sources */, - A8D6D2FF2B6C87F80061B11F /* PaddingConfigurationView.swift in Sources */, + A8A1C51E2BD3705A00515A14 /* PaddingConfigurationView.swift in Sources */, A8E1575F298654960005761C /* AboutView.swift in Sources */, - A8DCC98A2981F43F00D41065 /* PreviewSettingsView.swift in Sources */, A87376F62AA288EB001890F4 /* Window.swift in Sources */, A8B5E1632B43726C00044D30 /* CustomWindowActionAnchor.swift in Sources */, A8055EC22AFEDE0B00459D13 /* Keycorder.swift in Sources */, @@ -602,45 +590,48 @@ A8330AC52A3AC15900673C8D /* Notification+Extensions.swift in Sources */, A8878A252AA3B2C800850A66 /* WindowTransformAnimation.swift in Sources */, A827409A2AB0208500B9BDC5 /* TriggerKeycorder.swift in Sources */, - A85FEEBC2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift in Sources */, + A858914B2BDC5D3F00C10FB1 /* AdvancedConfiguration.swift in Sources */, + A82B1AEE2BD352A100E2F3F9 /* AccentColorConfiguration.swift in Sources */, A8D5A7D82A913862004EA5BB /* DirectionSelectorCircleSegment.swift in Sources */, A89307312BAE6D0100566AEE /* CustomWindowActionUnit.swift in Sources */, A83667C82A3D7D910001D630 /* AXUIElement+Extensions.swift in Sources */, A87F78962BAE333C0087B1DE /* CustomWindowActionPositionMode.swift in Sources */, A8330AD42A3AC27600673C8D /* WindowDirection.swift in Sources */, - 117C89682BA4CD6F00A32CE7 /* NSImage+Extensions.swift in Sources */, + A81D8D0A2C068B8700188E12 /* LuminarePreviewView.swift in Sources */, A8789F6929805B340040512E /* PreviewView.swift in Sources */, - A84497C92B393595003D4CF3 /* CustomKeybindView.swift in Sources */, A882660829809F6F00BCB197 /* GeneralSettingsView.swift in Sources */, A8E59C50298045D90064D4BA /* PreviewController.swift in Sources */, A81989062AC8EDB300EFF7A1 /* MenuBarHeaderText.swift in Sources */, + A81B98182BDC854F005FD78C /* AboutConfiguration.swift in Sources */, A86CB7332A3D22E7006A78F2 /* WindowEngine.swift in Sources */, A8E59C39297F5E9A0064D4BA /* LoopApp.swift in Sources */, A8330ACB2A3AC1C000673C8D /* Angle+Extensions.swift in Sources */, A8D6D3052B6C92F20061B11F /* WallpaperView.swift in Sources */, - A8F0125D2AEDFEC70017307F /* KeybindingsSettingsView.swift in Sources */, + A81D8D0C2C06950000188E12 /* LuminareManager.swift in Sources */, A864F4682AA660CD00579738 /* WindowDragManager.swift in Sources */, A8504D2D2A85832F00C2EFDA /* SoftwareUpdater.swift in Sources */, - A86DAE2A2B3CCF2900B968F0 /* CustomCyclingKeybindView.swift in Sources */, A8330ACF2A3AC1E900673C8D /* View+Extensions.swift in Sources */, + A8427E662C02594E00F20759 /* ExcludedAppsConfiguration.swift in Sources */, A82521EE29E235AC00139654 /* PermissionsManager.swift in Sources */, A8E59C4A297F98670064D4BA /* RadialMenuController.swift in Sources */, 0AFE802E2BB98E81009CF06F /* WindowDirection+LocalizedString.swift in Sources */, + A8A583B82BE5A117005F4CB2 /* CycleActionConfigurationView.swift in Sources */, + A8A1C5212BD4863B00515A14 /* KeybindingsConfiguration.swift in Sources */, A88E83C52B37B354009D332F /* CGEvent+Extensions.swift in Sources */, A8E8903C2BA7AAFE006C5074 /* NSEvent+Extensions.swift in Sources */, - 0A6DC3EB2BB869DE002AB05F /* WindowDirection+Image.swift in Sources */, + 0A6DC3EB2BB869DE002AB05F /* WindowAction+Image.swift in Sources */, A83E1C352ABFCA3200853FE9 /* WindowRecords.swift in Sources */, + A85DDBDA2C1693D4008C103D /* WindowDirection+Snapping.swift in Sources */, A81989082AC8F2AE00EFF7A1 /* MenuBarResizeButton.swift in Sources */, - A84497CB2B395D8C003D4CF3 /* AnchorPicker.swift in Sources */, A87DDD152B50A6A400A32C76 /* ScreenManager.swift in Sources */, A86949862A8F2BB70051AAAF /* CGKeyCode+Extensions.swift in Sources */, A8063A732B19891900EAB3D9 /* grid.metal in Sources */, - A8063A6E2B19599D00EAB3D9 /* SettingsView.swift in Sources */, A8F0125B2AEDD7660017307F /* WindowAction.swift in Sources */, + A8BC77792C0EB4DD008E2EDA /* AppDelegate+UNNotifications.swift in Sources */, A80397D42A932993006D2796 /* MenuBarIconView.swift in Sources */, A85B560E2AAAD62C00386ACE /* EventMonitor.swift in Sources */, - 116CE7302B9F21520014C19D /* ExcludeListSettingsView.swift in Sources */, A8A2ABE72A3FB0370067B5A9 /* KeybindMonitor.swift in Sources */, + A893D3642BD3299000063510 /* IconConfiguration.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -773,17 +764,16 @@ A8E59C45297F5E9B0064D4BA /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Classic"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-Developer"; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = ""; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Loop/Loop.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Loop/Preview Content\""; - DEVELOPMENT_TEAM = 8P53VAPX2H; + DEVELOPMENT_TEAM = 5F967GYF84; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -814,12 +804,11 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Loop/Loop.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Loop/Preview Content\""; - DEVELOPMENT_TEAM = 8P53VAPX2H; + DEVELOPMENT_TEAM = 5F967GYF84; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -865,15 +854,14 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - 116CE7332B9F31580014C19D /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/apple/swift-algorithms.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.2.0; - }; +/* Begin XCLocalSwiftPackageReference section */ + A8C83C692C0D1D560024A7A1 /* XCLocalSwiftPackageReference "../Luminare" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../Luminare; }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCRemoteSwiftPackageReference section */ A80397D02A93287C006D2796 /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MenuBarExtraAccess"; @@ -882,14 +870,6 @@ minimumVersion = 1.0.5; }; }; - A8063A6F2B195C9100EAB3D9 /* XCRemoteSwiftPackageReference "SettingsAccess" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/orchetect/SettingsAccess"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 1.4.0; - }; - }; A8DCC9792980D5F500D41065 /* XCRemoteSwiftPackageReference "Defaults" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/Defaults"; @@ -914,21 +894,15 @@ package = A80397D02A93287C006D2796 /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */; productName = MenuBarExtraAccess; }; - A8063A702B195C9100EAB3D9 /* SettingsAccess */ = { - isa = XCSwiftPackageProductDependency; - package = A8063A6F2B195C9100EAB3D9 /* XCRemoteSwiftPackageReference "SettingsAccess" */; - productName = SettingsAccess; - }; - A84F9E282BA4F20C00CF3E09 /* Algorithms */ = { - isa = XCSwiftPackageProductDependency; - package = 116CE7332B9F31580014C19D /* XCRemoteSwiftPackageReference "swift-algorithms" */; - productName = Algorithms; - }; A86DDA012BA4F6E900C0DFF7 /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = A8F0636E2985B2220010C33D /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; + A8C83C6A2C0D1D560024A7A1 /* Luminare */ = { + isa = XCSwiftPackageProductDependency; + productName = Luminare; + }; A8DCC97A2980D5F500D41065 /* Defaults */ = { isa = XCSwiftPackageProductDependency; package = A8DCC9792980D5F500D41065 /* XCRemoteSwiftPackageReference "Defaults" */; diff --git a/Loop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/Loop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/Loop.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,5 @@ + + + + + diff --git a/Loop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Loop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..e24fe29c --- /dev/null +++ b/Loop.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,33 @@ +{ + "originHash" : "c2a93a9d7a879ed92e3a49ac894535c44c007c6516c13aaedf4acff1c43a7b4a", + "pins" : [ + { + "identity" : "defaults", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sindresorhus/Defaults", + "state" : { + "branch" : "main", + "revision" : "38925e3cfacf3fb89a81a35b1cd44fd5a5b7e0fa" + } + }, + { + "identity" : "menubarextraaccess", + "kind" : "remoteSourceControl", + "location" : "https://github.com/orchetect/MenuBarExtraAccess", + "state" : { + "revision" : "f5896b47e15e114975897354c7e1082c51a2bffd", + "version" : "1.0.5" + } + }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "branch" : "2.x", + "revision" : "e9989a8ae7d09c946d07d325d71414024bd1e2dd" + } + } + ], + "version" : 3 +} diff --git a/Loop/About Window/AboutView.swift b/Loop/About Window/AboutView.swift index 117ddb66..84f7b1eb 100644 --- a/Loop/About Window/AboutView.swift +++ b/Loop/About Window/AboutView.swift @@ -106,7 +106,7 @@ struct AboutView: View { .controlSize(.large) Button { - self.isShowingAcknowledgements = true + isShowingAcknowledgements = true } label: { Text("Acknowledgements") .foregroundColor(.primary) @@ -115,7 +115,7 @@ struct AboutView: View { .controlSize(.large) .popover(isPresented: $isShowingAcknowledgements) { VStack { - ForEach(0.. () + ) { + if response.actionIdentifier == "setIconAction", + let icon = response.notification.request.content.userInfo["icon"] as? String { + IconManager.setAppIcon(to: icon) + } + + completionHandler() + } + + // Implementation is necessary to show notifications even when the app has focus! + func userNotificationCenter( + _: UNUserNotificationCenter, + willPresent _: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> () + ) { + completionHandler([.banner]) + } + + static func requestNotificationAuthorization() { + UNUserNotificationCenter.current().requestAuthorization( + options: [.alert] + ) { accepted, error in + if !accepted { + print("User Notification access denied.") + } + + if let error { + print(error) + } + } + } + + private static func registerNotificationCategories() { + let setIconAction = UNNotificationAction( + identifier: "setIconAction", + title: .init(localized: .init("Notification/Set Icon: Action", defaultValue: "Set Current Icon")), + options: .destructive + ) + let notificationCategory = UNNotificationCategory( + identifier: "icon_unlocked", + actions: [setIconAction], + intentIdentifiers: [] + ) + UNUserNotificationCenter.current().setNotificationCategories([notificationCategory]) + } + + static func areNotificationsEnabled() -> Bool { + let group = DispatchGroup() + group.enter() + + var notificationsEnabled = false + + UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in + notificationsEnabled = notificationSettings.authorizationStatus != UNAuthorizationStatus.denied + group.leave() + } + + group.wait() + return notificationsEnabled + } + + static func sendNotification(_ content: UNMutableNotificationContent) { + let uuidString = UUID().uuidString + let request = UNNotificationRequest( + identifier: uuidString, + content: content, + trigger: nil + ) + + requestNotificationAuthorization() + registerNotificationCategories() + + UNUserNotificationCenter.current().add(request) + } + + static func sendNotification(_ title: String, _ body: String) { + let content = UNMutableNotificationContent() + + content.title = title + content.body = body + content.categoryIdentifier = UUID().uuidString + + AppDelegate.sendNotification(content) + } +} diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index c44b5d2f..586cb77c 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -5,13 +5,14 @@ // Created by Kai Azim on 2023-10-05. // -import SwiftUI import Defaults +import SwiftUI import UserNotifications -class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { - private let loopManager = LoopManager() - private let windowDragManager = WindowDragManager() +class AppDelegate: NSObject, NSApplicationDelegate { + static let loopManager = LoopManager() + static let windowDragManager = WindowDragManager() + static var isActive: Bool = false private var launchedAsLoginItem: Bool { guard let event = NSAppleEventManager.shared().currentAppleEvent else { return false } @@ -20,9 +21,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele event.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem } - func applicationDidFinishLaunching(_ notification: Notification) { - NSApp.setActivationPolicy(.accessory) - + func applicationDidFinishLaunching(_: Notification) { // Check & ask for accessibility access AccessibilityManager.requestAccess() UNUserNotificationCenter.current().delegate = self @@ -30,142 +29,36 @@ class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDele AppDelegate.requestNotificationAuthorization() IconManager.refreshCurrentAppIcon() - loopManager.startObservingKeys() - windowDragManager.addObservers() - - if !self.launchedAsLoginItem { - self.openSettings() + AppDelegate.loopManager.start() + AppDelegate.windowDragManager.addObservers() + + if !launchedAsLoginItem { + LuminareManager.open() + } else { + // Dock icon is usually handled by LuminareManager, but in this case, it is manually set + if !Defaults[.showDockIcon] { + NSApp.setActivationPolicy(.accessory) + } } } - func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - NSApp.setActivationPolicy(.accessory) - for window in NSApp.windows where window.delegate != nil { - window.delegate = nil - } + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + LuminareManager.fullyClose() return false } - func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { - self.openSettings() + func applicationShouldHandleReopen(_: NSApplication, hasVisibleWindows _: Bool) -> Bool { + LuminareManager.open() return true } - // Mostly taken from https://github.com/Wouter01/SwiftUI-WindowManagement - func openSettings() { - // Settings window is already open - guard !NSApp.windows.contains(where: { $0.toolbar?.items != nil }) else { - NSApp.windows.first { $0.toolbar?.items != nil }?.orderFrontRegardless() - - return - } - - let eventSource = CGEventSource(stateID: .hidSystemState) - let keyCommand = CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode.kVK_ANSI_Comma, keyDown: true) - guard let keyCommand else { return } - - keyCommand.flags = .maskCommand - let event = NSEvent(cgEvent: keyCommand) - guard let event else { return } - - NSApp.sendEvent(event) - - for window in NSApp.windows where window.toolbar?.items != nil { - window.orderFrontRegardless() - window.center() - } - } - - // ---------- - // MARK: - Notifications - // ---------- - - func userNotificationCenter( - _: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void - ) { - if response.actionIdentifier == "setIconAction", - let icon = response.notification.request.content.userInfo["icon"] as? String { - IconManager.setAppIcon(to: icon) - } - - completionHandler() - } - - // Implementation is necessary to show notifications even when the app has focus! - func userNotificationCenter( - _: UNUserNotificationCenter, - willPresent _: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void - ) { - completionHandler([.banner]) + func applicationWillBecomeActive(_: Notification) { + Notification.Name.activeStateChanged.post(object: true) + AppDelegate.isActive = true } - static func requestNotificationAuthorization() { - UNUserNotificationCenter.current().requestAuthorization( - options: [.alert] - ) { accepted, error in - if !accepted { - print("User Notification access denied.") - } - - if let error = error { - print(error) - } - } - } - - private static func registerNotificationCategories() { - let setIconAction = UNNotificationAction( - identifier: "setIconAction", - title: .init(localized: .init("Notification/Set Icon: Action", defaultValue: "Set Current Icon")), - options: .destructive - ) - let notificationCategory = UNNotificationCategory( - identifier: "icon_unlocked", - actions: [setIconAction], - intentIdentifiers: [] - ) - UNUserNotificationCenter.current().setNotificationCategories([notificationCategory]) - } - - static func areNotificationsEnabled() -> Bool { - let group = DispatchGroup() - group.enter() - - var notificationsEnabled = false - - UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in - notificationsEnabled = notificationSettings.authorizationStatus != UNAuthorizationStatus.denied - group.leave() - } - - group.wait() - return notificationsEnabled - } - - static func sendNotification(_ content: UNMutableNotificationContent) { - let uuidString = UUID().uuidString - let request = UNNotificationRequest( - identifier: uuidString, - content: content, - trigger: nil - ) - - requestNotificationAuthorization() - registerNotificationCategories() - - UNUserNotificationCenter.current().add(request) - } - - static func sendNotification(_ title: String, _ body: String) { - let content = UNMutableNotificationContent() - - content.title = title - content.body = body - content.categoryIdentifier = UUID().uuidString - - AppDelegate.sendNotification(content) + func applicationWillResignActive(_: Notification) { + Notification.Name.activeStateChanged.post(object: false) + AppDelegate.isActive = false } } diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Contents.json b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Contents.json new file mode 100644 index 00000000..46cae255 --- /dev/null +++ b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "Developer-16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "Developer-32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "Developer-32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "Developer-64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "Developer-128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "Developer-256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "Developer-256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "Developer-512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "Developer-512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "Developer-1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-1024.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-1024.png new file mode 100644 index 00000000..03d52ff8 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-1024.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-128.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-128.png new file mode 100644 index 00000000..733005c0 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-128.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-16.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-16.png new file mode 100644 index 00000000..10410285 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-16.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-256.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-256.png new file mode 100644 index 00000000..3eb37490 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-256.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-32.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-32.png new file mode 100644 index 00000000..14f16cc9 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-32.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-512.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-512.png new file mode 100644 index 00000000..7aaca8aa Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-512.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-64.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-64.png new file mode 100644 index 00000000..3a823948 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Developer.appiconset/Developer-64.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Contents.json b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Contents.json new file mode 100644 index 00000000..8fd19395 --- /dev/null +++ b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "filename" : "Summer-16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "Summer-32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "Summer-32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "Summer-64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "Summer-128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "Summer-256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "Summer-256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "Summer-512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "Summer-512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "Summer-1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-1024.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-1024.png new file mode 100644 index 00000000..f69d21e9 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-1024.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-128.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-128.png new file mode 100644 index 00000000..47db3b4b Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-128.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-16.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-16.png new file mode 100644 index 00000000..65ee1f22 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-16.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-256.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-256.png new file mode 100644 index 00000000..d9823617 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-256.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-32.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-32.png new file mode 100644 index 00000000..9d8820c2 Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-32.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-512.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-512.png new file mode 100644 index 00000000..445b2cad Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-512.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-64.png b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-64.png new file mode 100644 index 00000000..40ac030a Binary files /dev/null and b/Loop/Assets.xcassets/App Icons/JSDev/AppIcon-Summer.appiconset/Summer-64.png differ diff --git a/Loop/Assets.xcassets/App Icons/JSDev/Contents.json b/Loop/Assets.xcassets/App Icons/JSDev/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Loop/Assets.xcassets/App Icons/JSDev/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/Assets.xcassets/Nucleo/12px_clipboard.imageset/12px_clipboard.pdf b/Loop/Assets.xcassets/Nucleo/12px_clipboard.imageset/12px_clipboard.pdf new file mode 100644 index 00000000..9f839a06 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/12px_clipboard.imageset/12px_clipboard.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/12px_clipboard.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/12px_clipboard.imageset/Contents.json new file mode 100644 index 00000000..d5249a4f --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/12px_clipboard.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "12px_clipboard.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/12px_share-up-right.imageset/12px_share-up-right.pdf b/Loop/Assets.xcassets/Nucleo/12px_share-up-right.imageset/12px_share-up-right.pdf new file mode 100644 index 00000000..9693962b Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/12px_share-up-right.imageset/12px_share-up-right.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/12px_share-up-right.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/12px_share-up-right.imageset/Contents.json new file mode 100644 index 00000000..36aa4616 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/12px_share-up-right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "12px_share-up-right.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_badge-check-2.imageset/18px_badge-check-2.pdf b/Loop/Assets.xcassets/Nucleo/18px_badge-check-2.imageset/18px_badge-check-2.pdf new file mode 100644 index 00000000..b628fadc Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_badge-check-2.imageset/18px_badge-check-2.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_badge-check-2.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_badge-check-2.imageset/Contents.json new file mode 100644 index 00000000..71f141cd --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_badge-check-2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_badge-check-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_color-palette.imageset/18px_color-palette.pdf b/Loop/Assets.xcassets/Nucleo/18px_color-palette.imageset/18px_color-palette.pdf new file mode 100644 index 00000000..2d1bfd1e Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_color-palette.imageset/18px_color-palette.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_color-palette.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_color-palette.imageset/Contents.json new file mode 100644 index 00000000..6cf23ea6 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_color-palette.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_color-palette.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_command.imageset/18px_command.pdf b/Loop/Assets.xcassets/Nucleo/18px_command.imageset/18px_command.pdf new file mode 100644 index 00000000..1f30f447 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_command.imageset/18px_command.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_command.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_command.imageset/Contents.json new file mode 100644 index 00000000..97313ea0 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_command.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_command.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_face-nerd-smile.imageset/18px_face-nerd-smile.pdf b/Loop/Assets.xcassets/Nucleo/18px_face-nerd-smile.imageset/18px_face-nerd-smile.pdf new file mode 100644 index 00000000..a97f9b9e Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_face-nerd-smile.imageset/18px_face-nerd-smile.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_face-nerd-smile.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_face-nerd-smile.imageset/Contents.json new file mode 100644 index 00000000..38d83914 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_face-nerd-smile.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_face-nerd-smile.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_finder.imageset/18px_finder.pdf b/Loop/Assets.xcassets/Nucleo/18px_finder.imageset/18px_finder.pdf new file mode 100644 index 00000000..c9dbb743 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_finder.imageset/18px_finder.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_finder.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_finder.imageset/Contents.json new file mode 100644 index 00000000..8ed49e95 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_finder.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_finder.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_gear.imageset/18px_gear.pdf b/Loop/Assets.xcassets/Nucleo/18px_gear.imageset/18px_gear.pdf new file mode 100644 index 00000000..e7dd7dbb Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_gear.imageset/18px_gear.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_gear.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_gear.imageset/Contents.json new file mode 100644 index 00000000..d9264184 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_gear.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_gear.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_lock.imageset/18px_lock.pdf b/Loop/Assets.xcassets/Nucleo/18px_lock.imageset/18px_lock.pdf new file mode 100644 index 00000000..5f239379 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_lock.imageset/18px_lock.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_lock.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_lock.imageset/Contents.json new file mode 100644 index 00000000..7cd18535 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_msg-smile-2.imageset/18px_msg-smile-2.pdf b/Loop/Assets.xcassets/Nucleo/18px_msg-smile-2.imageset/18px_msg-smile-2.pdf new file mode 100644 index 00000000..4005897e Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_msg-smile-2.imageset/18px_msg-smile-2.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_msg-smile-2.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_msg-smile-2.imageset/Contents.json new file mode 100644 index 00000000..aeeaeffd --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_msg-smile-2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_msg-smile-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_paintbrush.imageset/18px_paintbrush.pdf b/Loop/Assets.xcassets/Nucleo/18px_paintbrush.imageset/18px_paintbrush.pdf new file mode 100644 index 00000000..02455582 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_paintbrush.imageset/18px_paintbrush.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_paintbrush.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_paintbrush.imageset/Contents.json new file mode 100644 index 00000000..db791332 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_paintbrush.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_paintbrush.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_pen-2.imageset/18px_pen-2.pdf b/Loop/Assets.xcassets/Nucleo/18px_pen-2.imageset/18px_pen-2.pdf new file mode 100644 index 00000000..18ea29dc Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_pen-2.imageset/18px_pen-2.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_pen-2.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_pen-2.imageset/Contents.json new file mode 100644 index 00000000..178c6c2f --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_pen-2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_pen-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_repeat-4.imageset/18px_repeat-4.pdf b/Loop/Assets.xcassets/Nucleo/18px_repeat-4.imageset/18px_repeat-4.pdf new file mode 100644 index 00000000..36f07d5c Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_repeat-4.imageset/18px_repeat-4.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_repeat-4.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_repeat-4.imageset/Contents.json new file mode 100644 index 00000000..24cc809e --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_repeat-4.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_repeat-4.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_return-key.imageset/18px_return-key.pdf b/Loop/Assets.xcassets/Nucleo/18px_return-key.imageset/18px_return-key.pdf new file mode 100644 index 00000000..19538e91 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_return-key.imageset/18px_return-key.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_return-key.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_return-key.imageset/Contents.json new file mode 100644 index 00000000..ba6a40e8 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_return-key.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_return-key.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_ruler-pen.imageset/18px_ruler-pen.pdf b/Loop/Assets.xcassets/Nucleo/18px_ruler-pen.imageset/18px_ruler-pen.pdf new file mode 100644 index 00000000..7d314fab Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_ruler-pen.imageset/18px_ruler-pen.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_ruler-pen.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_ruler-pen.imageset/Contents.json new file mode 100644 index 00000000..9d797fdb --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_ruler-pen.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_ruler-pen.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_ruler.imageset/18px_ruler.pdf b/Loop/Assets.xcassets/Nucleo/18px_ruler.imageset/18px_ruler.pdf new file mode 100644 index 00000000..76e9ec0b Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_ruler.imageset/18px_ruler.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_ruler.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_ruler.imageset/Contents.json new file mode 100644 index 00000000..c588c0d7 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_ruler.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_ruler.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_shape-square.imageset/18px_shape-square.pdf b/Loop/Assets.xcassets/Nucleo/18px_shape-square.imageset/18px_shape-square.pdf new file mode 100644 index 00000000..d5116bd7 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_shape-square.imageset/18px_shape-square.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_shape-square.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_shape-square.imageset/Contents.json new file mode 100644 index 00000000..53af41f2 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_shape-square.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_shape-square.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_sidebar-right-2.imageset/18px_sidebar-right-2.pdf b/Loop/Assets.xcassets/Nucleo/18px_sidebar-right-2.imageset/18px_sidebar-right-2.pdf new file mode 100644 index 00000000..f433a7a4 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_sidebar-right-2.imageset/18px_sidebar-right-2.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_sidebar-right-2.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_sidebar-right-2.imageset/Contents.json new file mode 100644 index 00000000..e5dca13c --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_sidebar-right-2.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_sidebar-right-2.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_size.imageset/18px_size.pdf b/Loop/Assets.xcassets/Nucleo/18px_size.imageset/18px_size.pdf new file mode 100644 index 00000000..c12ba949 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_size.imageset/18px_size.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_size.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_size.imageset/Contents.json new file mode 100644 index 00000000..4f946e50 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_size.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_size.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_sliders.imageset/18px_sliders.pdf b/Loop/Assets.xcassets/Nucleo/18px_sliders.imageset/18px_sliders.pdf new file mode 100644 index 00000000..42afee50 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_sliders.imageset/18px_sliders.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_sliders.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_sliders.imageset/Contents.json new file mode 100644 index 00000000..1ce3df7c --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_sliders.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_sliders.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_square-sparkle.imageset/18px_square-sparkle.pdf b/Loop/Assets.xcassets/Nucleo/18px_square-sparkle.imageset/18px_square-sparkle.pdf new file mode 100644 index 00000000..cf6bcdd5 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_square-sparkle.imageset/18px_square-sparkle.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_square-sparkle.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_square-sparkle.imageset/Contents.json new file mode 100644 index 00000000..8031de8c --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_square-sparkle.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_square-sparkle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_table-lock.imageset/18px_table-lock.pdf b/Loop/Assets.xcassets/Nucleo/18px_table-lock.imageset/18px_table-lock.pdf new file mode 100644 index 00000000..bece8528 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_table-lock.imageset/18px_table-lock.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_table-lock.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_table-lock.imageset/Contents.json new file mode 100644 index 00000000..efeb378d --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_table-lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_table-lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_table-rows-3-cols-3.imageset/18px_table-rows-3-cols-3.pdf b/Loop/Assets.xcassets/Nucleo/18px_table-rows-3-cols-3.imageset/18px_table-rows-3-cols-3.pdf new file mode 100644 index 00000000..668c9e9a Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_table-rows-3-cols-3.imageset/18px_table-rows-3-cols-3.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_table-rows-3-cols-3.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_table-rows-3-cols-3.imageset/Contents.json new file mode 100644 index 00000000..ffee7ebd --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_table-rows-3-cols-3.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_table-rows-3-cols-3.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/18px_window-lock.imageset/18px_window-lock.pdf b/Loop/Assets.xcassets/Nucleo/18px_window-lock.imageset/18px_window-lock.pdf new file mode 100644 index 00000000..abd85270 Binary files /dev/null and b/Loop/Assets.xcassets/Nucleo/18px_window-lock.imageset/18px_window-lock.pdf differ diff --git a/Loop/Assets.xcassets/Nucleo/18px_window-lock.imageset/Contents.json b/Loop/Assets.xcassets/Nucleo/18px_window-lock.imageset/Contents.json new file mode 100644 index 00000000..b7dea246 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/18px_window-lock.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "18px_window-lock.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Loop/Assets.xcassets/Nucleo/Contents.json b/Loop/Assets.xcassets/Nucleo/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Loop/Assets.xcassets/Nucleo/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Loop/Assets.xcassets/custom.arrow.down.grow.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.down.grow.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.down.grow.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.down.grow.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.down.grow.rectangle.symbolset/custom.arrow.down.grow.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.down.grow.rectangle.symbolset/custom.arrow.down.grow.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.down.grow.rectangle.symbolset/custom.arrow.down.grow.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.down.grow.rectangle.symbolset/custom.arrow.down.grow.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.down.shrink.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.down.shrink.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.down.shrink.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.down.shrink.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.down.shrink.rectangle.symbolset/custom.arrow.down.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.down.shrink.rectangle.symbolset/custom.arrow.down.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.down.shrink.rectangle.symbolset/custom.arrow.down.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.down.shrink.rectangle.symbolset/custom.arrow.down.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.left.grow.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.left.grow.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.left.grow.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.left.grow.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.left.grow.rectangle.symbolset/custom.arrow.left.grow.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.left.grow.rectangle.symbolset/custom.arrow.left.grow.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.left.grow.rectangle.symbolset/custom.arrow.left.grow.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.left.grow.rectangle.symbolset/custom.arrow.left.grow.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.left.shrink.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.left.shrink.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.left.shrink.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.left.shrink.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.left.shrink.rectangle.symbolset/custom.arrow.left.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.left.shrink.rectangle.symbolset/custom.arrow.left.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.left.shrink.rectangle.symbolset/custom.arrow.left.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.left.shrink.rectangle.symbolset/custom.arrow.left.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.right.grow.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.right.grow.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.right.grow.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.right.grow.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.right.grow.rectangle.symbolset/custom.arrow.right.grow.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.right.grow.rectangle.symbolset/custom.arrow.right.grow.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.right.grow.rectangle.symbolset/custom.arrow.right.grow.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.right.grow.rectangle.symbolset/custom.arrow.right.grow.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.right.shrink.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.right.shrink.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.right.shrink.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.right.shrink.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.right.shrink.rectangle.symbolset/custom.arrow.right.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.right.shrink.rectangle.symbolset/custom.arrow.right.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.right.shrink.rectangle.symbolset/custom.arrow.right.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.right.shrink.rectangle.symbolset/custom.arrow.right.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.up.grow.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.up.grow.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.up.grow.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.up.grow.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.up.grow.rectangle.symbolset/custom.arrow.up.grow.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.up.grow.rectangle.symbolset/custom.arrow.up.grow.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.up.grow.rectangle.symbolset/custom.arrow.up.grow.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.up.grow.rectangle.symbolset/custom.arrow.up.grow.rectangle.svg diff --git a/Loop/Assets.xcassets/custom.arrow.up.shrink.rectangle.symbolset/Contents.json b/Loop/Assets.xcassets/Window Actions/custom.arrow.up.shrink.rectangle.symbolset/Contents.json similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.up.shrink.rectangle.symbolset/Contents.json rename to Loop/Assets.xcassets/Window Actions/custom.arrow.up.shrink.rectangle.symbolset/Contents.json diff --git a/Loop/Assets.xcassets/custom.arrow.up.shrink.rectangle.symbolset/custom.arrow.up.rectangle.svg b/Loop/Assets.xcassets/Window Actions/custom.arrow.up.shrink.rectangle.symbolset/custom.arrow.up.rectangle.svg similarity index 100% rename from Loop/Assets.xcassets/custom.arrow.up.shrink.rectangle.symbolset/custom.arrow.up.rectangle.svg rename to Loop/Assets.xcassets/Window Actions/custom.arrow.up.shrink.rectangle.symbolset/custom.arrow.up.rectangle.svg diff --git a/Loop/Extensions/AXUIElement+Extensions.swift b/Loop/Extensions/AXUIElement+Extensions.swift index d33d700a..22ee10c5 100644 --- a/Loop/Extensions/AXUIElement+Extensions.swift +++ b/Loop/Extensions/AXUIElement+Extensions.swift @@ -27,19 +27,19 @@ extension AXUIElement { @discardableResult func setValue(_ attribute: NSAccessibility.Attribute, value: Bool) -> Bool { - return setValue(attribute, value: value as CFBoolean) + setValue(attribute, value: value as CFBoolean) } @discardableResult func setValue(_ attribute: NSAccessibility.Attribute, value: CGPoint) -> Bool { guard let axValue = AXValue.from(value: value, type: .cgPoint) else { return false } - return self.setValue(attribute, value: axValue) + return setValue(attribute, value: axValue) } @discardableResult func setValue(_ attribute: NSAccessibility.Attribute, value: CGSize) -> Bool { guard let axValue = AXValue.from(value: value, type: .cgSize) else { return false } - return self.setValue(attribute, value: axValue) + return setValue(attribute, value: axValue) } func performAction(_ action: String) { @@ -65,14 +65,14 @@ extension AXUIElement { } extension NSAccessibility.Attribute { - static let fullScreen: NSAccessibility.Attribute = NSAccessibility.Attribute(rawValue: "AXFullScreen") + static let fullScreen: NSAccessibility.Attribute = .init(rawValue: "AXFullScreen") static let enhancedUserInterface = NSAccessibility.Attribute(rawValue: "AXEnhancedUserInterface") static let windowIds = NSAccessibility.Attribute(rawValue: "AXWindowsIDs") } extension AXValue { static func from(value: Any, type: AXValueType) -> AXValue? { - return withUnsafePointer(to: value) { ptr in + withUnsafePointer(to: value) { ptr in AXValueCreate(type, ptr) } } diff --git a/Loop/Extensions/Angle+Extensions.swift b/Loop/Extensions/Angle+Extensions.swift index a7bbee43..12c41836 100644 --- a/Loop/Extensions/Angle+Extensions.swift +++ b/Loop/Extensions/Angle+Extensions.swift @@ -9,16 +9,16 @@ import SwiftUI extension Angle { func normalized() -> Angle { - let degrees = (self.degrees.truncatingRemainder(dividingBy: 360) + 360) - .truncatingRemainder(dividingBy: 360) + let degrees = (degrees.truncatingRemainder(dividingBy: 360) + 360) + .truncatingRemainder(dividingBy: 360) return Angle(degrees: degrees) } func angleDifference(to angle2: Angle) -> Angle { - let angle1 = self.degrees + let angle1 = degrees let angle2 = angle2.degrees - let diff: Double = ( angle2 - angle1 + 180.0 ).truncatingRemainder(dividingBy: 360.0) - 180.0 + let diff: Double = (angle2 - angle1 + 180.0).truncatingRemainder(dividingBy: 360.0) - 180.0 return Angle(degrees: diff < -180 ? diff + 360 : diff) } } diff --git a/Loop/Extensions/Bundle+Extensions.swift b/Loop/Extensions/Bundle+Extensions.swift index 3f8b928b..4ae1d983 100644 --- a/Loop/Extensions/Bundle+Extensions.swift +++ b/Loop/Extensions/Bundle+Extensions.swift @@ -9,7 +9,7 @@ import Foundation // Returns the current build number extension Bundle { - var appName: String { getInfo("CFBundleName") } + var appName: String { getInfo("CFBundleName") } var displayName: String { getInfo("CFBundleDisplayName") } var bundleID: String { getInfo("CFBundleIdentifier") } var copyright: String { getInfo("NSHumanReadableCopyright") } diff --git a/Loop/Extensions/CGGeometry+Extensions.swift b/Loop/Extensions/CGGeometry+Extensions.swift index 38a37562..62d83854 100644 --- a/Loop/Extensions/CGGeometry+Extensions.swift +++ b/Loop/Extensions/CGGeometry+Extensions.swift @@ -1,5 +1,5 @@ // -// CGPoint+Extensions.swift +// CGGeometry+Extensions.swift // Loop // // Created by Kai Azim on 2023-06-14. @@ -9,7 +9,7 @@ import SwiftUI extension CGFloat { func approximatelyEquals(to comparison: CGFloat, tolerance: CGFloat = 10) -> Bool { - return abs(self - comparison) < tolerance + abs(self - comparison) < tolerance } } @@ -31,40 +31,40 @@ extension CGPoint { } func flipY(maxY: CGFloat) -> CGPoint { - CGPoint(x: self.x, y: maxY - self.y) + CGPoint(x: x, y: maxY - y) } func flipY(screen: NSScreen) -> CGPoint { - return flipY(maxY: screen.frame.maxY) + flipY(maxY: screen.frame.maxY) } func approximatelyEqual(to point: CGPoint, tolerance: CGFloat = 10) -> Bool { abs(x - point.x) < tolerance && - abs(y - point.y) < tolerance + abs(y - point.y) < tolerance } } extension CGSize { var area: CGFloat { - self.width * self.height + width * height } func approximatelyEqual(to size: CGSize, tolerance: CGFloat = 10) -> Bool { - return abs(width - size.width) < tolerance && abs(height - size.height) < tolerance + abs(width - size.width) < tolerance && abs(height - size.height) < tolerance } } extension CGRect { func flipY(screen: NSScreen) -> CGRect { - return flipY(maxY: screen.frame.maxY) + flipY(maxY: screen.frame.maxY) } func flipY(maxY: CGFloat) -> CGRect { CGRect( - x: self.minX, + x: minX, y: maxY - self.maxY, - width: self.width, - height: self.height + width: width, + height: height ) } @@ -93,10 +93,10 @@ extension CGRect { } func approximatelyEqual(to rect: CGRect, tolerance: CGFloat = 10) -> Bool { - return abs(origin.x - rect.origin.x) < tolerance && - abs(origin.y - rect.origin.y) < tolerance && - abs(width - rect.width) < tolerance && - abs(height - rect.height) < tolerance + abs(origin.x - rect.origin.x) < tolerance && + abs(origin.y - rect.origin.y) < tolerance && + abs(width - rect.width) < tolerance && + abs(height - rect.height) < tolerance } func pushBottomRightPointInside(_ rect2: CGRect) -> CGRect { @@ -114,33 +114,33 @@ extension CGRect { } var topLeftPoint: CGPoint { - CGPoint(x: self.minX, y: self.minY) + CGPoint(x: minX, y: minY) } var topRightPoint: CGPoint { - CGPoint(x: self.maxX, y: self.minY) + CGPoint(x: maxX, y: minY) } var bottomLeftPoint: CGPoint { - CGPoint(x: self.minX, y: self.maxY) + CGPoint(x: minX, y: maxY) } var bottomRightPoint: CGPoint { - CGPoint(x: self.maxX, y: self.maxY) + CGPoint(x: maxX, y: maxY) } var center: CGPoint { - CGPoint(x: self.midX, y: self.midY) + CGPoint(x: midX, y: midY) } func inset(by amount: CGFloat, minSize: CGSize) -> CGRect { // Respect minimum width and height - let insettedWidth = max(minSize.width, self.width - 2 * amount) - let insettedHeight = max(minSize.height, self.height - 2 * amount) + let insettedWidth = max(minSize.width, width - 2 * amount) + let insettedHeight = max(minSize.height, height - 2 * amount) // Calculate the new inset rectangle - let newX = self.midX - insettedWidth / 2 - let newY = self.midY - insettedHeight / 2 + let newX = midX - insettedWidth / 2 + let newY = midY - insettedHeight / 2 return CGRect( x: newX, @@ -153,19 +153,19 @@ extension CGRect { func getEdgesTouchingBounds(_ rect2: CGRect) -> Edge.Set { var result: Edge.Set = [] - if self.minX.approximatelyEquals(to: rect2.minX) { + if minX.approximatelyEquals(to: rect2.minX) { result.insert(.leading) } - if self.minY.approximatelyEquals(to: rect2.minY) { + if minY.approximatelyEquals(to: rect2.minY) { result.insert(.top) } - if self.maxX.approximatelyEquals(to: rect2.maxX) { + if maxX.approximatelyEquals(to: rect2.maxX) { result.insert(.trailing) } - if self.maxY.approximatelyEquals(to: rect2.maxY) { + if maxY.approximatelyEquals(to: rect2.maxY) { result.insert(.bottom) } diff --git a/Loop/Extensions/CGKeyCode+Extensions.swift b/Loop/Extensions/CGKeyCode+Extensions.swift index cc452622..2f1d643c 100644 --- a/Loop/Extensions/CGKeyCode+Extensions.swift +++ b/Loop/Extensions/CGKeyCode+Extensions.swift @@ -6,8 +6,8 @@ // // From https://gist.github.com/chrispaynter/07c9b16219c3d58f57a6e2b0249db4bf (but edited a lot) -import CoreGraphics import Carbon +import CoreGraphics import SwiftUI extension CGKeyCode { @@ -166,11 +166,11 @@ extension CGKeyCode { } var isModifier: Bool { - return (.kVK_RightCommand ... .kVK_Function).contains(self) + (.kVK_RightCommand ... .kVK_Function).contains(self) } var isOnRightSide: Bool { - return [.kVK_RightCommand, .kVK_RightControl, .kVK_RightOption, .kVK_RightShift].contains(self) + [.kVK_RightCommand, .kVK_RightControl, .kVK_RightOption, .kVK_RightShift].contains(self) } var isPressed: Bool { @@ -229,14 +229,14 @@ extension CGKeyCode { // There's "⌧“ 'X In A Rectangle Box' (U+2327), "☒" 'Ballot Box with X' (U+2612), "×" 'Multiplication Sign' (U+00d7), "⨯" 'Vector or Cross Product' (U+2a2f), or a plain small x. All combined symbols appear bigger. .kVK_ANSI_KeypadClear: "☒\u{20e3}", // The combined symbol appears bigger than the other combined 'keycaps' // TODO: Respect locale decimal separator ("." or ",") - .kVK_ANSI_KeypadDecimal: ".\u{20e3}", + .kVK_ANSI_KeypadDecimal: ".\u{20e3}", .kVK_ANSI_KeypadDivide: "/\u{20e3}", // "⏎" 'Return Symbol' (U+23CE) but "↩" 'Leftwards Arrow with Hook' (U+00d7) seems to be more common on macOS. .kVK_ANSI_KeypadEnter: "↩\u{20e3}", // The combined symbol appears bigger than the other combined 'keycaps' .kVK_ANSI_KeypadEquals: "=\u{20e3}", .kVK_ANSI_KeypadMinus: "-\u{20e3}", .kVK_ANSI_KeypadMultiply: "*\u{20e3}", - .kVK_ANSI_KeypadPlus: "+\u{20e3}", + .kVK_ANSI_KeypadPlus: "+\u{20e3}" ] // Make sure to use baseModifier before using this! @@ -322,17 +322,17 @@ extension CGKeyCode { } var systemImage: String? { - if let systemName = CGKeyCode.keyToImage[self.baseModifier] { - return systemName + if let systemName = CGKeyCode.keyToImage[baseModifier] { + systemName } else { - return nil + nil } } } extension NSEvent.ModifierFlags { func convertToCGKeyCode() -> Set { - let deviceIndependent = self.intersection(.deviceIndependentFlagsMask) + let deviceIndependent = intersection(.deviceIndependentFlagsMask) var result: Set = [] if deviceIndependent.contains(.command) { diff --git a/Loop/Extensions/CaseIterable+Extensions.swift b/Loop/Extensions/CaseIterable+Extensions.swift deleted file mode 100644 index 2de98d9b..00000000 --- a/Loop/Extensions/CaseIterable+Extensions.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// CaseIterable+Extensions.swift -// Loop -// -// Created by Kai Azim on 2023-06-14. -// - -import Foundation - -extension CaseIterable where Self: Equatable { - func next() -> Self { - let all = Self.allCases - let idx = all.firstIndex(of: self)! - let next = all.index(after: idx) - return all[next == all.endIndex ? all.startIndex : next] - } -} diff --git a/Loop/Extensions/Color+Extensions.swift b/Loop/Extensions/Color+Extensions.swift index 6413f5c7..3f942b19 100644 --- a/Loop/Extensions/Color+Extensions.swift +++ b/Loop/Extensions/Color+Extensions.swift @@ -5,14 +5,15 @@ // Created by Kai Azim on 2023-09-11. // -import SwiftUI import Defaults +import SwiftUI extension Color { enum LoopAccentTone { case normal case darker } + static func getLoopAccent(tone: LoopAccentTone) -> Color { switch tone { case .normal: @@ -27,4 +28,8 @@ extension Color { return Defaults[.gradientColor] } } + + static var systemGray: Color { + Color(nsColor: NSColor.systemGray.blended(withFraction: 0.2, of: .black)!) + } } diff --git a/Loop/Extensions/Defaults+Extensions.swift b/Loop/Extensions/Defaults+Extensions.swift index f0387659..fa662b06 100644 --- a/Loop/Extensions/Defaults+Extensions.swift +++ b/Loop/Extensions/Defaults+Extensions.swift @@ -5,50 +5,58 @@ // Created by Kai Azim on 2023-06-14. // -import SwiftUI import Defaults +import SwiftUI // Add variables for default values (which are stored even then the app is closed) extension Defaults.Keys { - static let hapticFeedback = Defaults.Key("hapticFeedback", default: true) - static let launchAtLogin = Key("launchAtLogin", default: false) - static let hideMenuBarIcon = Key("hideMenuBarIcon", default: false) + // Icon static let currentIcon = Key("currentIcon", default: "AppIcon-Classic") - static let notificationWhenIconUnlocked = Key("notificationWhenIconUnlocked", default: true) static let timesLooped = Key("timesLooped", default: 0) - static let windowSnapping = Key("windowSnapping", default: false) // BETA - static let animateWindowResizes = Key("animateWindowResizes", default: false) // BETA - static let padding = Key("padding", default: .zero) - static let restoreWindowFrameOnDrag = Key("restoreWindowFrameOnDrag", default: false) - static let resizeWindowUnderCursor = Key("resizeWindowUnderCursor", default: false) - static let focusWindowOnResize = Key("focusWindowOnResize", default: true) - static let animationConfiguration = Key("animationConfiguration", default: .smooth) + static let showDockIcon = Key("showDockIcon", default: false) + static let notificationWhenIconUnlocked = Key("notificationWhenIconUnlocked", default: true) + // Accent Color static let useSystemAccentColor = Key("useSystemAccentColor", default: true) static let customAccentColor = Key("customAccentColor", default: Color(.white)) static let useGradient = Key("useGradient", default: true) static let gradientColor = Key("gradientColor", default: Color(.black)) + // Radial Menu static let radialMenuVisibility = Key("radialMenuVisibility", default: true) + static let disableCursorInteraction = Key("disableCursorInteraction", default: false) static let radialMenuCornerRadius = Key("radialMenuCornerRadius", default: 50) static let radialMenuThickness = Key("radialMenuThickness", default: 22) - static let hideUntilDirectionIsChosen = Key("hideUntilDirectionIsChosen", default: false) - static let disableCursorInteraction = Key("disableCursorInteraction", default: false) - - static let triggerKey = Key>("trigger", default: [.kVK_Function]) - static let doubleClickToTrigger = Key("doubleClickToTrigger", default: false) - static let triggerDelay = Key("triggerDelay", default: 0) - static let middleClickTriggersLoop = Key("middleClickTriggersLoop", default: false) + // Preview static let previewVisibility = Key("previewVisibility", default: true) - static let previewCornerRadius = Key("previewCornerRadius", default: 10) static let previewPadding = Key("previewPadding", default: 10) + static let previewCornerRadius = Key("previewCornerRadius", default: 10) static let previewBorderThickness = Key("previewBorderThickness", default: 5) + // Behavior + static let launchAtLogin = Key("launchAtLogin", default: false) + static let hideMenuBarIcon = Key("hideMenuBarIcon", default: false) + static let animationConfiguration = Key("animationConfiguration", default: .fast) + static let windowSnapping = Key("windowSnapping", default: false) + static let restoreWindowFrameOnDrag = Key("restoreWindowFrameOnDrag", default: false) + static let enablePadding = Key("enablePadding", default: false) + static let padding = Key("padding", default: .zero) + static let useScreenWithCursor = Key("useScreenWithCursor", default: true) + static let moveCursorWithWindow = Key("moveCursorWithWindow", default: false) + static let resizeWindowUnderCursor = Key("resizeWindowUnderCursor", default: false) + static let focusWindowOnResize = Key("focusWindowOnResize", default: true) + static let respectStageManager = Key("respectStageManager", default: true) + static let stageStripSize = Key("stageStripSize", default: 150) + + // Keybindings + static let triggerKey = Key>("trigger", default: [.kVK_Function]) + static let triggerDelay = Key("triggerDelay", default: 0) + static let doubleClickToTrigger = Key("doubleClickToTrigger", default: false) + static let middleClickTriggersLoop = Key("middleClickTriggersLoop", default: false) static let keybinds = Key<[WindowAction]>("keybinds", default: [ WindowAction(.maximize, keybind: [.kVK_Space]), WindowAction(.center, keybind: [.kVK_Return]), - WindowAction(.init(localized: .init("Top Cycle", defaultValue: "Top Cycle")), [ .init(.topHalf), .init(.topThird), @@ -76,12 +84,14 @@ extension Defaults.Keys { WindowAction(.bottomLeftQuarter, keybind: [.kVK_DownArrow, .kVK_LeftArrow]) ]) - static let applicationExcludeList = Key<[String]>("applicationExcludeList", default: []) + // Advanced + static let animateWindowResizes = Key("animateWindowResizes", default: false) // BETA + static let hideUntilDirectionIsChosen = Key("hideUntilDirectionIsChosen", default: false) + static let hapticFeedback = Defaults.Key("hapticFeedback", default: true) - static let respectStageManager = Key("respectStageManager", default: true) - static let stageStripSize = Key("stageStripSize", default: 150) + // About + static let includeDevelopmentVersions = Key("includeDevelopmentVersions", default: false) + static let excludedApps = Key<[URL]>("excludedApps", default: []) static let sizeIncrement = Key("sizeIncrement", default: 20) - - static let includeDevelopmentVersions = Key("includeDevelopmentVersions", default: false) } diff --git a/Loop/Extensions/NSEvent+Extensions.swift b/Loop/Extensions/NSEvent+Extensions.swift index 96c3cde7..262d593d 100644 --- a/Loop/Extensions/NSEvent+Extensions.swift +++ b/Loop/Extensions/NSEvent+Extensions.swift @@ -9,6 +9,6 @@ import SwiftUI extension NSEvent.ModifierFlags { var wasKeyUp: Bool { - self.rawValue == 256 || self.rawValue == 65792 + rawValue == 256 || rawValue == 65792 } } diff --git a/Loop/Extensions/NSImage+Extensions.swift b/Loop/Extensions/NSImage+Extensions.swift deleted file mode 100644 index c8c7153d..00000000 --- a/Loop/Extensions/NSImage+Extensions.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// NSImage+Extensions.swift -// Loop -// -// Created by Dirk Mika on 15.03.24. -// - -import Foundation -import AppKit - -extension NSImage { - func resized(to newSize: NSSize) -> NSImage { - return NSImage(size: newSize, flipped: false) { rect in - self.draw( - in: rect, - from: NSRect(origin: CGPoint.zero, size: self.size), - operation: NSCompositingOperation.copy, - fraction: 1.0 - ) - return true - } - } -} diff --git a/Loop/Extensions/NSScreen+Extensions.swift b/Loop/Extensions/NSScreen+Extensions.swift index a2fbd1d3..742629cb 100644 --- a/Loop/Extensions/NSScreen+Extensions.swift +++ b/Loop/Extensions/NSScreen+Extensions.swift @@ -5,15 +5,15 @@ // Created by Kai Azim on 2023-06-14. // -import SwiftUI import Defaults +import SwiftUI extension NSScreen { // Return the CGDirectDisplayID // Used in to help calculate the size a window needs to be resized to var displayID: CGDirectDisplayID? { let key = NSDeviceDescriptionKey("NSScreenNumber") - return self.deviceDescription[key] as? CGDirectDisplayID + return deviceDescription[key] as? CGDirectDisplayID } static var screenWithMouse: NSScreen? { @@ -26,15 +26,14 @@ extension NSScreen { var safeScreenFrame: CGRect { guard - let displayID = self.displayID + let displayID else { print("ERROR: Failed to get NSScreen.displayID in NSScreen.safeScreenFrame") - return self.frame.flipY(screen: self) + return frame.flipY(screen: self) } let screenFrame = CGDisplayBounds(displayID) - let visibleFrame = self.stageStripFreeFrame.flipY(maxY: self.frame.maxY) - let menubarHeight = visibleFrame.origin.y + let visibleFrame = stageStripFreeFrame.flipY(screen: self) // By setting safeScreenFrame to visibleFrame, we won't need to adjust its size. var safeScreenFrame = visibleFrame @@ -50,9 +49,9 @@ extension NSScreen { } var stageStripFreeFrame: NSRect { - var frame = self.visibleFrame + var frame = visibleFrame - if Defaults[.respectStageManager] && StageManager.enabled && StageManager.shown { + if Defaults[.respectStageManager], StageManager.enabled, StageManager.shown { if StageManager.position == .leading { frame.origin.x += Defaults[.stageStripSize] } @@ -65,12 +64,16 @@ extension NSScreen { var displayBounds: CGRect { guard - let displayID = self.displayID + let displayID else { print("ERROR: Failed to get NSScreen.displayID in NSScreen.displayBounds") - return self.frame.flipY(screen: self) + return frame.flipY(screen: self) } return CGDisplayBounds(displayID) } + + var menubarHeight: CGFloat { + frame.maxY - visibleFrame.maxY + } } diff --git a/Loop/Extensions/Notification+Extensions.swift b/Loop/Extensions/Notification+Extensions.swift index 2f9c7a0f..80d8435c 100644 --- a/Loop/Extensions/Notification+Extensions.swift +++ b/Loop/Extensions/Notification+Extensions.swift @@ -13,10 +13,11 @@ extension Notification.Name { static let forceCloseLoop = Notification.Name("forceCloseLoop") static let didLoop = Notification.Name("didLoop") + static let activeStateChanged = Notification.Name("activeStateChanged") @discardableResult - func onReceive(object: Any? = nil, using: @escaping (Notification) -> Void) -> NSObjectProtocol { - return NotificationCenter.default.addObserver( + func onReceive(object: Any? = nil, using: @escaping (Notification) -> ()) -> NSObjectProtocol { + NotificationCenter.default.addObserver( forName: self, object: object, queue: .main, diff --git a/Loop/Extensions/Optional+Extensions.swift b/Loop/Extensions/Optional+Extensions.swift deleted file mode 100644 index 7770ba25..00000000 --- a/Loop/Extensions/Optional+Extensions.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// Optional+Extensions.swift -// Loop -// -// Created by Kai Azim on 2023-12-26. -// - -import Foundation - -extension Optional where Wrapped == String { - public var bound: String { - get { - return self ?? "" - } - set { - self = newValue.isEmpty ? nil : newValue - } - } -} diff --git a/Loop/Extensions/UNNotification+Extensions.swift b/Loop/Extensions/UNNotification+Extensions.swift index 73476687..76328c1d 100644 --- a/Loop/Extensions/UNNotification+Extensions.swift +++ b/Loop/Extensions/UNNotification+Extensions.swift @@ -24,13 +24,13 @@ extension UNNotificationAttachment { try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil) let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier) try imgData.write(to: fileURL!, options: []) - let imageAttachment = try UNNotificationAttachment.init( + let imageAttachment = try UNNotificationAttachment( identifier: imageFileIdentifier, url: fileURL!, options: nil ) return imageAttachment - } catch let error { + } catch { print("error \(error)") } diff --git a/Loop/Extensions/View+Extensions.swift b/Loop/Extensions/View+Extensions.swift index 900126e6..2973612b 100644 --- a/Loop/Extensions/View+Extensions.swift +++ b/Loop/Extensions/View+Extensions.swift @@ -13,24 +13,11 @@ extension View { _ name: Notification.Name, center: NotificationCenter = .default, object: AnyObject? = nil, - perform action: @escaping (Notification) -> Void + perform action: @escaping (Notification) -> () ) -> some View { - self.onReceive( + onReceive( center.publisher(for: name, object: object), perform: action ) } - - /// Applies the given transform if the given condition evaluates to `true`. - /// - Parameters: - /// - condition: The condition to evaluate. - /// - transform: The transform to apply to the source `View`. - /// - Returns: Either the original `View` or the modified `View` if the condition is `true`. - @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { - if condition { - transform(self) - } else { - self - } - } } diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 557ccf92..1fa0fd0f 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -3,6 +3,9 @@ "strings" : { "" : { + }, + "%" : { + }, "%@" : { @@ -18,6 +21,7 @@ } }, "%@ Coordinates" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -28,6 +32,7 @@ } }, "%@ Custom" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -38,6 +43,7 @@ } }, "%@ Generic" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -48,6 +54,7 @@ } }, "%@ Initial Size" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -68,6 +75,7 @@ } }, "%@ Percentages" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -78,6 +86,7 @@ } }, "%@ Pixels" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -86,8 +95,12 @@ } } } + }, + "%@ places windows slightly above the absolute center,\nwhich can be found more ergonomic." : { + }, "%@ Preserve Size" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -98,6 +111,7 @@ } }, "%@'s notification permissions are currently disabled." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -107,16 +121,6 @@ } } }, - "%@%@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "%1$@%2$@" - } - } - } - }, "%lld" : { }, @@ -212,6 +216,7 @@ } }, "Adjusts window frame in real-time as you choose a direction." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -232,6 +237,7 @@ } }, "ALPHA" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -240,8 +246,12 @@ } } } + }, + "Animate window resize" : { + }, "Animate windows being resized" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -272,6 +282,7 @@ } }, "Appearance" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -282,6 +293,7 @@ } }, "Applications in the exclude list are ignored by %@." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -310,6 +322,12 @@ } } } + }, + "Border thickness" : { + + }, + "Bottom" : { + }, "Bottom Cycle" : { "localizations" : { @@ -320,8 +338,18 @@ } } } + }, + "Cannot be enabled when the preview is disabled." : { + + }, + "Change" : { + + }, + "Check for updates…" : { + }, "Check for Updates…" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -330,8 +358,21 @@ } } } + }, + "Click on 'Send Feedback' to go to our GitHub page, where you can report bugs, suggest new features, or provide other valuable input." : { + + }, + "Close" : { + + }, + "Color" : { + + }, + "Configure padding..." : { + }, "Configure…" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -340,9 +381,15 @@ } } } + }, + "Corner radius" : { + + }, + "Credits" : { + }, "Crisp Value Adjuster: Border Thickness" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -359,7 +406,7 @@ } }, "Crisp Value Adjuster: Bottom" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -376,7 +423,7 @@ } }, "Crisp Value Adjuster: Corner Radius" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -393,7 +440,7 @@ } }, "Crisp Value Adjuster: External Bar" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -410,7 +457,7 @@ } }, "Crisp Value Adjuster: External Bar Description" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -427,7 +474,7 @@ } }, "Crisp Value Adjuster: Height" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -444,7 +491,7 @@ } }, "Crisp Value Adjuster: Left" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -461,7 +508,7 @@ } }, "Crisp Value Adjuster: Padding" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -478,7 +525,7 @@ } }, "Crisp Value Adjuster: Right" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -495,7 +542,7 @@ } }, "Crisp Value Adjuster: Size Increment" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -512,7 +559,7 @@ } }, "Crisp Value Adjuster: Size Increment Description" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -529,7 +576,7 @@ } }, "Crisp Value Adjuster: Stage Strip Size" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -546,7 +593,7 @@ } }, "Crisp Value Adjuster: Thickness" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -563,7 +610,7 @@ } }, "Crisp Value Adjuster: Top" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -580,7 +627,7 @@ } }, "Crisp Value Adjuster: Trigger Delay" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -597,7 +644,7 @@ } }, "Crisp Value Adjuster: Width" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -614,7 +661,7 @@ } }, "Crisp Value Adjuster: Window Gaps" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -630,29 +677,8 @@ } } }, - "Crisp Value Adjuster: X" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "X" - } - } - } - }, - "Crisp Value Adjuster: Y" : { - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Y" - } - } - } - }, "Current version: %@ (%@)" : { + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -667,8 +693,15 @@ } } } + }, + "Cursor" : { + + }, + "Custom" : { + }, "Custom accent color" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -679,6 +712,7 @@ } }, "Custom cycle" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -699,6 +733,7 @@ } }, "Custom gradient color" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -709,6 +744,7 @@ } }, "Custom keybind" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -729,6 +765,7 @@ } }, "Custom screen padding" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -737,6 +774,9 @@ } } } + }, + "Cycle Keybind" : { + }, "Default notification content" : { "extractionState" : "extracted_with_value", @@ -756,6 +796,7 @@ } }, "Delete" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -764,6 +805,15 @@ } } } + }, + "Design" : { + + }, + "Development" : { + + }, + "Development support" : { + }, "Disable cursor interaction" : { "localizations" : { @@ -774,6 +824,9 @@ } } } + }, + "Disable radial menu cursor interaction" : { + }, "Donate…" : { "localizations" : { @@ -786,6 +839,7 @@ } }, "Done" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -794,8 +848,12 @@ } } } + }, + "Double-click to trigger" : { + }, "Double-click trigger key to trigger %@" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -806,6 +864,7 @@ } }, "Excluded Applications" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -834,6 +893,9 @@ } } } + }, + "External bar" : { + }, "Fast" : { "localizations" : { @@ -846,6 +908,7 @@ } }, "Feedback" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -877,8 +940,12 @@ }, "GitHub" : { + }, + "Gradient" : { + }, "Granted" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -889,7 +956,7 @@ } }, "Greg Lassale Footer" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -934,8 +1001,15 @@ } } } + }, + "Height" : { + + }, + "Hide menu bar icon" : { + }, "Hide menu until direction is chosen" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -946,6 +1020,7 @@ } }, "Hide menubar icon" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -954,6 +1029,9 @@ } } } + }, + "Hide until direction is chosen" : { + }, "Horizontal Thirds" : { "localizations" : { @@ -964,6 +1042,12 @@ } } } + }, + "Icon" : { + + }, + "Icon contributor" : { + }, "Icon Name: Black" : { "extractionState" : "extracted_with_value", @@ -1016,6 +1100,17 @@ } } }, + "Icon Name: Developer" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Developer" + } + } + } + }, "Icon Name: Holo" : { "extractionState" : "extracted_with_value", "localizations" : { @@ -1135,6 +1230,17 @@ } } }, + "Icon Name: Summer" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Summer" + } + } + } + }, "Icon Name: Synthwave Sunset" : { "extractionState" : "extracted_with_value", "localizations" : { @@ -1239,6 +1345,9 @@ } } } + }, + "Include padding" : { + }, "Instant" : { "localizations" : { @@ -1334,6 +1443,7 @@ } }, "Measurement unit" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1342,8 +1452,12 @@ } } } + }, + "Middle-click to trigger" : { + }, "Middle-click to trigger %@" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1372,8 +1486,12 @@ } } } + }, + "Move cursor with window" : { + }, "Name" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1382,8 +1500,12 @@ } } } + }, + "No excluded applications" : { + }, "No Excluded Applications" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1414,6 +1536,7 @@ } }, "Not granted" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1451,6 +1574,7 @@ } }, "Notify when new icons are unlocked" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1459,8 +1583,12 @@ } } } + }, + "Notify when unlocking new icons" : { + }, "Open notification settings" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1469,6 +1597,9 @@ } } } + }, + "Options" : { + }, "Padding" : { "localizations" : { @@ -1491,6 +1622,7 @@ } }, "Please turn them on in System Settings." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1501,6 +1633,7 @@ } }, "Position mode" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1509,8 +1642,18 @@ } } } + }, + "Press \"Add\" to add a cycle item" : { + + }, + "Press \"Add\" to add a keybind" : { + + }, + "Press \"Add\" to add an application" : { + }, "Press + to add a cycle item!" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1521,6 +1664,7 @@ } }, "Press + to add a keybind!" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1531,6 +1675,7 @@ } }, "Press + to add an application!" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1541,6 +1686,7 @@ } }, "Press a key…" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1561,6 +1707,7 @@ } }, "Preview window size" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1599,6 +1746,9 @@ } } } + }, + "Radial menu" : { + }, "Radial Menu" : { "localizations" : { @@ -1611,6 +1761,7 @@ } }, "Re-open %@ to see this window." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1621,6 +1772,7 @@ } }, "Request Access…" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1629,6 +1781,12 @@ } } } + }, + "Request…" : { + + }, + "Reset" : { + }, "Resize window under cursor" : { "localizations" : { @@ -1651,6 +1809,7 @@ } }, "Resizes frontmost window." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1661,6 +1820,7 @@ } }, "Resizes window under cursor, and uses the frontmost window as backup." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1681,6 +1841,7 @@ } }, "Restore Defaults" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1721,6 +1882,7 @@ } }, "Screen Padding" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1786,6 +1948,7 @@ } }, "sec" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1795,7 +1958,19 @@ } } }, + "Seconds" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "s" + } + } + } + }, "Selected icon:" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1816,6 +1991,7 @@ } }, "Sending feedback will bring you to our \"New Issue\" GitHub page, where you can report a bug, request a feature & more!" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1834,6 +2010,9 @@ } } } + }, + "Settings" : { + }, "Settings…" : { "localizations" : { @@ -1846,6 +2025,7 @@ } }, "Show" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1854,6 +2034,9 @@ } } } + }, + "Show in dock" : { + }, "Show preview when looping" : { "localizations" : { @@ -1866,6 +2049,7 @@ } }, "Show radial menu when looping" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1874,8 +2058,15 @@ } } } + }, + "Simple" : { + + }, + "Size increment" : { + }, "Sizing mode" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1894,6 +2085,9 @@ } } } + }, + "Some features, ideas, and bug fixes" : { + }, "Stage Manager" : { "localizations" : { @@ -1904,6 +2098,15 @@ } } } + }, + "Stage strip size" : { + + }, + "Suggest new icon" : { + + }, + "System" : { + }, "That key is already used as your trigger key." : { "localizations" : { @@ -1944,6 +2147,15 @@ } } } + }, + "Theming" : { + + }, + "Thickness" : { + + }, + "This feature is still under development." : { + }, "This is only needed to animate windows being resized." : { "extractionState" : "stale", @@ -1957,6 +2169,7 @@ } }, "This point determines the upper-left edge of the window." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1967,6 +2180,7 @@ } }, "This preview is not to scale." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1977,6 +2191,7 @@ } }, "Tip: To use Caps Lock, remap it to control in System Settings!" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1985,6 +2200,9 @@ } } } + }, + "Top" : { + }, "Top Cycle" : { "localizations" : { @@ -1995,8 +2213,12 @@ } } } + }, + "Trigger delay" : { + }, "Trigger key" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2017,6 +2239,7 @@ } }, "Updates" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2025,8 +2248,12 @@ } } } + }, + "Use coordinates" : { + }, "Use gradient" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2035,8 +2262,12 @@ } } } + }, + "Use macOS center" : { + }, "Use macOS Center" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2045,8 +2276,15 @@ } } } + }, + "Use pixels" : { + + }, + "Use screen with cursor" : { + }, "Use system accent color" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2055,6 +2293,9 @@ } } } + }, + "Use this if you are using a custom menubar." : { + }, "Version %@ (%@)" : { "localizations" : { @@ -2081,9 +2322,15 @@ } } } + }, + "Width" : { + + }, + "Window" : { + }, "Window Direction/More Information: macOS Center" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2457,7 +2704,7 @@ } }, "Window Direction/Name: macOS Center" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2473,6 +2720,17 @@ } } }, + "Window Direction/Name: MacOS Center" : { + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "MacOS Center" + } + } + } + }, "Window Direction/Name: Maximize" : { "extractionState" : "extracted_with_value", "localizations" : { @@ -2812,8 +3070,12 @@ } } } + }, + "Window gaps" : { + }, "Window padding" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2824,6 +3086,7 @@ } }, "Window position" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2834,6 +3097,7 @@ } }, "Window size" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2854,6 +3118,7 @@ } }, "Windows will not animate their resizes." : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -2862,6 +3127,12 @@ } } } + }, + "X" : { + + }, + "Y" : { + }, "You can only use up to %lld keys in a keybind, including the trigger key." : { "localizations" : { diff --git a/Loop/LoopApp.swift b/Loop/LoopApp.swift index 430d892a..87043f45 100644 --- a/Loop/LoopApp.swift +++ b/Loop/LoopApp.swift @@ -5,10 +5,9 @@ // Created by Kai Azim on 2023-01-23. // -import SwiftUI -import MenuBarExtraAccess -import SettingsAccess import Defaults +import MenuBarExtraAccess +import SwiftUI @main struct LoopApp: App { @@ -18,13 +17,9 @@ struct LoopApp: App { @Default(.hideMenuBarIcon) var hideMenuBarIcon var body: some Scene { - Settings { - SettingsView() - } - MenuBarExtra("Loop", image: "empty", isInserted: Binding.constant(!hideMenuBarIcon)) { #if DEBUG - MenuBarHeaderText("DEV BUILD: \(Bundle.main.appVersion) (\(Bundle.main.appBuild))") + MenuBarHeaderText("DEV BUILD: \(Bundle.main.appVersion) (\(Bundle.main.appBuild))") #endif Button { @@ -61,22 +56,9 @@ struct LoopApp: App { ForEach(WindowDirection.verticalThirds) { MenuBarResizeButton($0) } } - SettingsLink( - label: { - Text("Settings…") - }, - preAction: { - for window in NSApp.windows where window.toolbar?.items != nil { - window.close() - } - }, - postAction: { - for window in NSApp.windows where window.toolbar?.items != nil { - window.orderFrontRegardless() - window.center() - } - } - ) + Button("Settings…") { + LuminareManager.open() + } .keyboardShortcut(",", modifiers: .command) Button("About \(Bundle.main.appName)") { @@ -96,7 +78,7 @@ struct LoopApp: App { .menuBarExtraAccess(isPresented: $isMenubarItemPresented) { statusItem in guard let button = statusItem.button, - button.subviews.count == 0 + button.subviews.isEmpty else { return } diff --git a/Loop/Luminare/Loop/AboutConfiguration.swift b/Loop/Luminare/Loop/AboutConfiguration.swift new file mode 100644 index 00000000..3a9fff21 --- /dev/null +++ b/Loop/Luminare/Loop/AboutConfiguration.swift @@ -0,0 +1,186 @@ +// +// AboutConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-26. +// + +import Defaults +import Luminare +import SwiftUI + +class AboutConfigurationModel: ObservableObject { + let currentIcon = Defaults[.currentIcon] // no need for didSet since it won't change here + @Published var includeDevelopmentVersions = Defaults[.includeDevelopmentVersions] { + didSet { + Defaults[.includeDevelopmentVersions] = includeDevelopmentVersions + } + } + + let credits: [CreditItem] = [ + .init( + "Kai", + "Development", + url: .init(string: "https://github.com/mrkai77")!, + avatar: .init(string: "https://github.com/mrkai77.png?size=200")! + ), + .init( + "Jace", + "Design", + url: .init(string: "https://x.com/jacethings")!, + avatar: .init(string: "https://github.com/soft-bred.png?size=200")! + ), + .init( + "Kami", + "Development support", + url: .init(string: "https://github.com/senpaihunters")!, + avatar: .init(string: "https://github.com/senpaihunters.png?size=200")! + ), + .init( + "Greg Lassale", + "Icon contributor", + url: .init(string: "https://x.com/greglassale")!, + avatar: .init(string: "https://pbs.twimg.com/profile_images/1746348765127094272/eNO2LxOQ_200x200.jpg")! + ), + .init( + "JSDev", + "Icon contributor", + url: .init(string: "https://github.com/N-coder82")!, + avatar: .init(string: "https://github.com/n-coder82.png?size=200")! + ), + .init( + "Contributors on GitHub", + "Some features, ideas, and bug fixes", + url: .init(string: "https://github.com/MrKai77/Loop/graphs/contributors")!, + avatar: .init(string: "https://github.githubassets.com/assets/GitHub-Mark-ea2971cee799.png?size=200")! + ) + ] + + func copyVersionToClipboard() { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString( + "Version \(Bundle.main.appVersion) (\(Bundle.main.appBuild))", + forType: NSPasteboard.PasteboardType.string + ) + } +} + +struct CreditItem: Identifiable { + var id: String { name } + + let name: String + let description: LocalizedStringKey? + let url: URL + let avatar: URL + + init(_ name: String, _ description: LocalizedStringKey? = nil, url: URL, avatar: URL) { + self.name = name + self.description = description + self.avatar = avatar + self.url = url + } +} + +struct AboutConfigurationView: View { + @Environment(\.openURL) private var openURL + @StateObject private var model = AboutConfigurationModel() + @StateObject var updater = SoftwareUpdater() + + var body: some View { + LuminareSection { + Button { + model.copyVersionToClipboard() + } label: { + HStack { + if let image = NSImage(named: model.currentIcon) { + Image(nsImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 60) + } + + VStack(alignment: .leading, spacing: 2) { + Text("Loop") + .fontWeight(.medium) + + Text("Version \(Bundle.main.appVersion) (\(Bundle.main.appBuild))") + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + } + .padding(4) + } + .buttonStyle(LuminareCosmeticButtonStyle(Image(._12PxClipboard))) + } + + LuminareSection { + Button("Check for updates…") { + updater.checkForUpdates() + } + + LuminareToggle("Automatically check for updates", isOn: $updater.automaticallyChecksForUpdates) + LuminareToggle("Include development versions", isOn: $model.includeDevelopmentVersions) + } + + LuminareSection { + VStack(alignment: .leading, spacing: 12) { + Text("Click on 'Send Feedback' to go to our GitHub page, where you can report bugs, suggest new features, or provide other valuable input.") + + Button("Send Feedback") { + openURL(URL(string: "https://github.com/MrKai77/Loop")!) + } + .buttonStyle(LuminareCompactButtonStyle()) + } + .padding(8) + } + + LuminareSection("Credits") { + ForEach(model.credits) { credit in + creditsView(credit) + } + } + } + + @ViewBuilder + func creditsView(_ credit: CreditItem) -> some View { + Button { + openURL(credit.url) + } label: { + HStack(spacing: 12) { + AsyncImage(url: credit.avatar) { image in + image + .resizable() + .aspectRatio(contentMode: .fit) + } placeholder: { + Image(systemName: "person.crop.circle") + .resizable() + .foregroundStyle(.tertiary) + .aspectRatio(contentMode: .fit) + } + .frame(height: 40) + .overlay { + Circle() + .strokeBorder(.white.opacity(0.1), lineWidth: 1) + } + .clipShape(.circle) + + VStack(alignment: .leading) { + Text(credit.name) + + if let description = credit.description { + Text(description) + .font(.caption) + .foregroundStyle(.secondary) + } + } + + Spacer() + } + .padding(12) + } + .buttonStyle(LuminareCosmeticButtonStyle(Image(._12PxShareUpRight))) + } +} diff --git a/Loop/Luminare/Loop/AdvancedConfiguration.swift b/Loop/Luminare/Loop/AdvancedConfiguration.swift new file mode 100644 index 00000000..6d305596 --- /dev/null +++ b/Loop/Luminare/Loop/AdvancedConfiguration.swift @@ -0,0 +1,141 @@ +// +// AdvancedConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-26. +// + +import Combine +import Defaults +import Luminare +import SwiftUI + +class AdvancedConfigurationModel: ObservableObject { + @Published var animateWindowResizes = Defaults[.animateWindowResizes] { + didSet { + Defaults[.animateWindowResizes] = animateWindowResizes + } + } + + @Published var hideUntilDirectionIsChosen = Defaults[.hideUntilDirectionIsChosen] { + didSet { + Defaults[.hideUntilDirectionIsChosen] = hideUntilDirectionIsChosen + } + } + + @Published var disableCursorInteraction = Defaults[.disableCursorInteraction] { + didSet { + Defaults[.disableCursorInteraction] = disableCursorInteraction + } + } + + @Published var hapticFeedback = Defaults[.hapticFeedback] { + didSet { + Defaults[.hapticFeedback] = hapticFeedback + } + } + + @Published var sizeIncrement = Defaults[.sizeIncrement] { + didSet { + Defaults[.sizeIncrement] = sizeIncrement + } + } + + @Published var isAccessibilityAccessGranted = AccessibilityManager.getStatus() + @Published var accessibilityChecker: Publishers.Autoconnect = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + @Published var accessibilityChecks: Int = 0 + + func beginAccessibilityAccessRequest() { + accessibilityChecker = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + accessibilityChecks = 0 + AccessibilityManager.requestAccess() + } + + func refreshAccessiblityStatus() { + accessibilityChecks += 1 + let isGranted = AccessibilityManager.getStatus() + + if isAccessibilityAccessGranted != isGranted { + withAnimation(.smooth) { + isAccessibilityAccessGranted = isGranted + } + } + + if isGranted || accessibilityChecks > 60 { + accessibilityChecker.upstream.connect().cancel() + } + } +} + +struct AdvancedConfigurationView: View { + @StateObject private var model = AdvancedConfigurationModel() + let elementHeight: CGFloat = 34 + + var body: some View { + LuminareSection("General") { + LuminareToggle( + "Animate window resize", + info: .init("This feature is still under development.", .orange), + isOn: $model.animateWindowResizes + ) + LuminareToggle("Disable radial menu cursor interaction", isOn: $model.disableCursorInteraction) + LuminareToggle("Hide until direction is chosen", isOn: $model.hideUntilDirectionIsChosen) + LuminareToggle("Haptic feedback", isOn: $model.hapticFeedback) + + LuminareValueAdjuster( + "Size increment", // Description: Used in size adjustment window actions + value: $model.sizeIncrement, + sliderRange: 5...50, + suffix: "px", + step: 4.5, + lowerClamp: true + ) + } + + LuminareSection("Keybinds") { + HStack(spacing: 2) { + Button("Import") { + WindowAction.importPrompt() + } + + Button("Export") { + WindowAction.exportPrompt() + } + + Button("Reset") { + Defaults.reset(.keybinds) + } + .buttonStyle(LuminareDestructiveButtonStyle()) + } + } + + LuminareSection("Permissions") { + HStack { + if model.isAccessibilityAccessGranted { + Image(._18PxBadgeCheck2) + .foregroundStyle(Color.getLoopAccent(tone: .normal)) + } + + Text("Accessibility access") + + Spacer() + + Button { + model.beginAccessibilityAccessRequest() + } label: { + Text("Request…") + .frame(height: 30) + .padding(.horizontal, 8) + } + .disabled(model.isAccessibilityAccessGranted) + .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) + } + .padding(.leading, 8) + .padding(.trailing, 2) + .frame(height: elementHeight) + } + .onReceive(model.accessibilityChecker) { _ in + model.refreshAccessiblityStatus() + } + } +} diff --git a/Loop/Luminare/Loop/ExcludedAppsConfiguration.swift b/Loop/Luminare/Loop/ExcludedAppsConfiguration.swift new file mode 100644 index 00000000..a515b6a5 --- /dev/null +++ b/Loop/Luminare/Loop/ExcludedAppsConfiguration.swift @@ -0,0 +1,164 @@ +// +// ExcludedAppsConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-05-25. +// + +import Defaults +import Luminare +import SwiftUI + +class ExcludedAppsConfigurationModel: ObservableObject { + @Published var excludedApps = Defaults[.excludedApps] { + didSet { + Defaults[.excludedApps] = excludedApps + } + } + + @Published var selectedApps = Set() + + func showAppChooser() { + DispatchQueue.main.async { + guard let window = LuminareManager.window else { return } + let panel = NSOpenPanel() + panel.worksWhenModal = true + panel.allowsMultipleSelection = true + panel.canChooseDirectories = false + panel.canChooseFiles = true + panel.allowedContentTypes = [.application] + panel.allowsOtherFileTypes = false + panel.resolvesAliases = true + panel.directoryURL = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask).first + panel.beginSheetModal(for: window) { result in + if result == .OK { + let appsToAdd = panel.urls.compactMap { self.excludedApps.contains($0) ? nil : $0 } + + withAnimation(.smooth(duration: 0.25)) { + self.excludedApps.append(contentsOf: appsToAdd) + } + } + } + } + } +} + +struct ExcludedAppsConfigurationView: View { + @StateObject private var model = ExcludedAppsConfigurationModel() + + var body: some View { + LuminareList( + items: $model.excludedApps, + selection: $model.selectedApps, + addAction: { + model.showAppChooser() + }, + content: { url in + AppView(url: url) + .equatable() + }, + emptyView: { + HStack { + Spacer() + VStack { + Text("No excluded applications") + .font(.title3) + Text("Press \"Add\" to add an application") + .font(.caption) + } + Spacer() + } + .foregroundStyle(.secondary) + .padding() + }, + id: \.self + ) + } +} + +struct AppView: View, Equatable { + @ObservedObject var app: App + + init(url: Binding) { + self.app = App(url: url.wrappedValue) ?? App( + bundleID: "unknown", + displayName: url.wrappedValue.lastPathComponent, + path: url.wrappedValue.relativePath, + url: url.wrappedValue.absoluteURL, + icon: .init(systemSymbolName: "exclamationmark.triangle", accessibilityDescription: nil) + ) + } + + var body: some View { + HStack(spacing: 8) { + Group { + if let icon = app.icon { + Image(nsImage: icon) + } else { + ProgressView() + } + } + .frame(width: 36, height: 36) + + VStack(alignment: .leading) { + Text(app.displayName) + + Text(app.path) + .foregroundStyle(.secondary) + .font(.caption) + } + + Spacer() + + Button { + NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: app.path)]) + } label: { + Image(._18PxFinder) + .foregroundStyle(.secondary) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(.horizontal, 12) + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.app.url == rhs.app.url + } + + class App: Identifiable, ObservableObject { + var id: String { bundleID } + let bundleID: String + @Published var icon: NSImage? + let displayName: String + let path: String + let url: URL + + init?(url: URL) { + guard + let meta = NSMetadataItem(url: url), + let bundleId = meta.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String, + let displayName = meta.value(forAttribute: NSMetadataItemDisplayNameKey) as? String, + let path = meta.value(forAttribute: NSMetadataItemPathKey) as? String + else { + return nil + } + + self.bundleID = bundleId + self.displayName = displayName + self.path = path + self.url = url + + DispatchQueue.main.async { + self.icon = NSWorkspace.shared.icon(forFile: self.path) + } + } + + init(bundleID: String, displayName: String, path: String, url: URL, icon: NSImage? = nil) { + self.bundleID = bundleID + self.displayName = displayName + self.path = path + self.url = url + self.icon = icon + } + } +} diff --git a/Loop/Luminare/LuminareManager.swift b/Loop/Luminare/LuminareManager.swift new file mode 100644 index 00000000..00c121b1 --- /dev/null +++ b/Loop/Luminare/LuminareManager.swift @@ -0,0 +1,105 @@ +// +// LuminareManager.swift +// Loop +// +// Created by Kai Azim on 2024-05-28. +// + +import Defaults +import Luminare +import SwiftUI + +class LuminareManager { + static var window: NSWindow? { + LuminareManager.luminare.windowController?.window + } + + static let iconConfiguration = SettingsTab("Icon", Image(._18PxSquareSparkle), IconConfigurationView()) + static let accentColorConfiguration = SettingsTab("Accent Color", Image(._18PxPaintbrush), AccentColorConfigurationView()) + static let radialMenuConfiguration = SettingsTab("Radial Menu", Image("loop"), RadialMenuConfigurationView()) + static let previewConfiguration = SettingsTab("Preview", Image(._18PxSidebarRight2), PreviewConfigurationView()) + + static let behaviorConfiguration = SettingsTab("Behavior", Image(._18PxGear), BehaviorConfigurationView()) + static let keybindingsConfiguration = SettingsTab("Keybindings", Image(._18PxCommand), KeybindingsConfigurationView()) + + static let advancedConfiguration = SettingsTab("Advanced", Image(._18PxFaceNerdSmile), AdvancedConfigurationView()) + static let excludedAppsConfiguration = SettingsTab("Excluded Apps", Image(._18PxWindowLock), ExcludedAppsConfigurationView()) + static let aboutConfiguration = SettingsTab("About", Image(._18PxMsgSmile2), AboutConfigurationView()) + + static var luminare = LuminareSettingsWindow( + [ + .init("Theming", [ + iconConfiguration, + accentColorConfiguration, + radialMenuConfiguration, + previewConfiguration + ]), + .init("Settings", [ + behaviorConfiguration, + keybindingsConfiguration + ]), + .init("Loop", [ + advancedConfiguration, + excludedAppsConfiguration, + aboutConfiguration + ]) + ], + tint: { + AppDelegate.isActive ? Color.getLoopAccent(tone: .normal) : Color.systemGray + }, + didTabChange: processTabChange + ) + + private static func processTabChange(_ tab: SettingsTab? = nil) { + DispatchQueue.main.async { + if tab == radialMenuConfiguration { + luminare.hidePreview(identifier: "Preview") + luminare.showPreview(identifier: "RadialMenu") + return + } + if tab == previewConfiguration { + luminare.showPreview(identifier: "Preview") + luminare.hidePreview(identifier: "RadialMenu") + return + } + if tab == accentColorConfiguration || tab == behaviorConfiguration { + luminare.showPreview(identifier: "Preview") + luminare.showPreview(identifier: "RadialMenu") + return + } + } + } + + static func open() { + if luminare.windowController == nil { + luminare.initializeWindow() + + DispatchQueue.main.async { + luminare.addPreview( + content: LuminarePreviewView(), + identifier: "Preview", + fullSize: true + ) + luminare.addPreview( + content: RadialMenuView(previewMode: true), + identifier: "RadialMenu" + ) + + luminare.showPreview(identifier: "Preview") + luminare.showPreview(identifier: "RadialMenu") + } + } + + luminare.show() + AppDelegate.isActive = true + NSApp.setActivationPolicy(.regular) + } + + static func fullyClose() { + luminare.deinitWindow() + + if !Defaults[.showDockIcon] { + NSApp.setActivationPolicy(.accessory) + } + } +} diff --git a/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift b/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift new file mode 100644 index 00000000..8e8e4d7a --- /dev/null +++ b/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift @@ -0,0 +1,158 @@ +// +// BehaviorConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-19. +// + +import Defaults +import Luminare +import ServiceManagement +import SwiftUI + +class BehaviorConfigurationModel: ObservableObject { + @Published var launchAtLogin = Defaults[.launchAtLogin] { + didSet { + Defaults[.launchAtLogin] = launchAtLogin + + if launchAtLogin { + try? SMAppService().register() + } else { + try? SMAppService().unregister() + } + } + } + + @Published var hideMenuBarIcon = Defaults[.hideMenuBarIcon] { + didSet { + Defaults[.hideMenuBarIcon] = hideMenuBarIcon + } + } + + @Published var animationConfiguration = Defaults[.animationConfiguration] { + didSet { + Defaults[.animationConfiguration] = animationConfiguration + } + } + + @Published var windowSnapping = Defaults[.windowSnapping] { + didSet { + Defaults[.windowSnapping] = windowSnapping + } + } + + @Published var restoreWindowFrameOnDrag = Defaults[.restoreWindowFrameOnDrag] { + didSet { + Defaults[.restoreWindowFrameOnDrag] = restoreWindowFrameOnDrag + } + } + + @Published var enablePadding = Defaults[.enablePadding] { + didSet { + Defaults[.enablePadding] = enablePadding + } + } + + @Published var useScreenWithCursor = Defaults[.useScreenWithCursor] { + didSet { + Defaults[.useScreenWithCursor] = useScreenWithCursor + } + } + + @Published var moveCursorWithWindow = Defaults[.moveCursorWithWindow] { + didSet { + Defaults[.moveCursorWithWindow] = moveCursorWithWindow + } + } + + @Published var resizeWindowUnderCursor = Defaults[.resizeWindowUnderCursor] { + didSet { + Defaults[.resizeWindowUnderCursor] = resizeWindowUnderCursor + } + } + + @Published var focusWindowOnResize = Defaults[.focusWindowOnResize] { + didSet { + Defaults[.focusWindowOnResize] = focusWindowOnResize + } + } + + @Published var respectStageManager = Defaults[.respectStageManager] { + didSet { + Defaults[.respectStageManager] = respectStageManager + } + } + + @Published var stageStripSize = Defaults[.stageStripSize] { + didSet { + Defaults[.stageStripSize] = stageStripSize + } + } + + @Published var isPaddingConfigurationViewPresented = false + + let previewVisibility = Defaults[.previewVisibility] +} + +struct BehaviorConfigurationView: View { + @StateObject private var model = BehaviorConfigurationModel() + + var body: some View { + LuminareSection("General") { + LuminareToggle("Launch at login", isOn: $model.launchAtLogin) + LuminareToggle("Hide menu bar icon", isOn: $model.hideMenuBarIcon) + LuminareSliderPicker( + "Animation speed", + AnimationConfiguration.allCases.reversed(), + selection: $model.animationConfiguration + ) { + $0.name + } + } + + LuminareSection("Window") { + LuminareToggle("Window snapping", isOn: $model.windowSnapping) + LuminareToggle("Restore window frame on drag", isOn: $model.restoreWindowFrameOnDrag) + LuminareToggle("Include padding", isOn: $model.enablePadding.animation(.smooth(duration: 0.25))) + + if model.enablePadding { + Button("Configure padding...") { + model.isPaddingConfigurationViewPresented = true + } + .luminareModal(isPresented: $model.isPaddingConfigurationViewPresented) { + PaddingConfigurationView(isPresented: $model.isPaddingConfigurationViewPresented) + .frame(width: 400) + } + } + } + + LuminareSection("Cursor") { + LuminareToggle("Use screen with cursor", isOn: $model.useScreenWithCursor) + LuminareToggle( + "Move cursor with window", + info: model.previewVisibility ? nil : .init("Cannot be enabled when the preview is disabled."), + isOn: $model.moveCursorWithWindow, + disabled: !model.previewVisibility + ) + LuminareToggle("Resize window under cursor", isOn: $model.resizeWindowUnderCursor.animation(.smooth(duration: 0.25))) + + if model.resizeWindowUnderCursor { + LuminareToggle("Focus window on resize", isOn: $model.focusWindowOnResize) + } + } + + LuminareSection("Stage Manager") { + LuminareToggle("Respect Stage Manager", isOn: $model.respectStageManager.animation(.smooth(duration: 0.25))) + + if model.respectStageManager { + LuminareValueAdjuster( + "Stage strip size", + value: $model.stageStripSize, + sliderRange: 50...200, + suffix: "px", + lowerClamp: true + ) + } + } + } +} diff --git a/Loop/Luminare/Settings/Behavior/Padding Configuration/PaddingConfigurationView.swift b/Loop/Luminare/Settings/Behavior/Padding Configuration/PaddingConfigurationView.swift new file mode 100644 index 00000000..e415e4a0 --- /dev/null +++ b/Loop/Luminare/Settings/Behavior/Padding Configuration/PaddingConfigurationView.swift @@ -0,0 +1,163 @@ +// +// PaddingConfigurationView.swift +// Loop +// +// Created by Kai Azim on 2024-04-19. +// + +import Defaults +import Luminare +import SwiftUI + +struct PaddingConfigurationView: View { + @State var paddingModel = Defaults[.padding] + @Binding var isPresented: Bool + + var body: some View { + Group { + ScreenView { + PaddingPreviewView($paddingModel) + } + + LuminareSection { + paddingMode() + + if !paddingModel.configureScreenPadding { + nonScreenPaddingConfiguration() + } else { + screenSidesPaddingConfiguration() + } + } + + if paddingModel.configureScreenPadding { + LuminareSection { + screenInsetsPaddingConfiguration() + } + } + + Button("Close") { + isPresented = false + } + .buttonStyle(LuminareCompactButtonStyle()) + } + .onChange(of: paddingModel) { _ in + // This fixes some weird animations. + Defaults[.padding] = paddingModel + } + } + + func paddingMode() -> some View { + LuminarePicker( + elements: [false, true], + selection: Binding( + get: { + paddingModel.configureScreenPadding + }, + set: { newValue in + withAnimation(.smooth(duration: 0.25)) { + paddingModel.configureScreenPadding = newValue + + if !paddingModel.configureScreenPadding { + paddingModel.window = 0 + paddingModel.top = 0 + paddingModel.bottom = 0 + paddingModel.right = 0 + paddingModel.left = 0 + } + } + } + ), + columns: 2, + roundBottom: false + ) { custom in + HStack(spacing: 6) { + if custom { + Image(._18PxSliders) + Text("Custom") + } else { + Image(._18PxShapeSquare) + Text("Simple") + } + } + .fixedSize() + } + } + + func nonScreenPaddingConfiguration() -> some View { + LuminareValueAdjuster( + "Padding", + value: Binding( + get: { + paddingModel.window + }, + set: { + paddingModel.window = $0 + paddingModel.top = $0 + paddingModel.bottom = $0 + paddingModel.right = $0 + paddingModel.left = $0 + } + ), + sliderRange: 0...100, + suffix: "px", + lowerClamp: true + ) + } + + func screenSidesPaddingConfiguration() -> some View { + Group { + LuminareValueAdjuster( + "Top", + value: $paddingModel.top, + sliderRange: 0...100, + suffix: "px", + lowerClamp: true, + controlSize: .compact + ) + LuminareValueAdjuster( + "Bottom", + value: $paddingModel.bottom, + sliderRange: 0...100, + suffix: "px", + lowerClamp: true, + controlSize: .compact + ) + LuminareValueAdjuster( + "Right", + value: $paddingModel.right, + sliderRange: 0...100, + suffix: "px", + lowerClamp: true, + controlSize: .compact + ) + LuminareValueAdjuster( + "Left", + value: $paddingModel.left, + sliderRange: 0...100, + suffix: "px", + lowerClamp: true, + controlSize: .compact + ) + } + } + + func screenInsetsPaddingConfiguration() -> some View { + Group { + LuminareValueAdjuster( + "Window gaps", + value: $paddingModel.window, + sliderRange: 0...100, + suffix: "px", + lowerClamp: true + ) + LuminareValueAdjuster( + "External bar", + info: .init("Use this if you are using a custom menubar."), + value: $paddingModel.externalBar, + sliderRange: 0...100, + suffix: "px", + lowerClamp: true + ) + } + } +} diff --git a/Loop/Settings/Padding/PaddingPreviewView.swift b/Loop/Luminare/Settings/Behavior/Padding Configuration/PaddingPreviewView.swift similarity index 65% rename from Loop/Settings/Padding/PaddingPreviewView.swift rename to Loop/Luminare/Settings/Behavior/Padding Configuration/PaddingPreviewView.swift index 7091b9e5..53fcc190 100644 --- a/Loop/Settings/Padding/PaddingPreviewView.swift +++ b/Loop/Luminare/Settings/Behavior/Padding Configuration/PaddingPreviewView.swift @@ -8,30 +8,31 @@ import SwiftUI struct PaddingPreviewView: View { - @Binding var paddingModel: PaddingModel + @Binding var model: PaddingModel init(_ paddingModel: Binding) { - self._paddingModel = paddingModel + self._model = paddingModel } var body: some View { GeometryReader { geo in ZStack { - HStack(spacing: paddingModel.window / 2) { + HStack(spacing: model.window / 2) { blurredWindow() - VStack(spacing: paddingModel.window / 2) { + VStack(spacing: model.window / 2) { blurredWindow() blurredWindow() } } - .padding(.top, paddingModel.totalTopPadding / 2) - .padding(.bottom, paddingModel.bottom / 2) - .padding(.leading, paddingModel.left / 2) - .padding(.trailing, paddingModel.right / 2) + .padding(.top, model.totalTopPadding / 2) + .padding(.bottom, model.bottom / 2) + .padding(.leading, model.left / 2) + .padding(.trailing, model.right / 2) } .frame(width: geo.size.width, height: geo.size.height) } + .animation(.smooth(duration: 0.25), value: model) } @ViewBuilder diff --git a/Loop/Luminare/Settings/Keybindings/CustomActionConfigurationView.swift b/Loop/Luminare/Settings/Keybindings/CustomActionConfigurationView.swift new file mode 100644 index 00000000..ebcaea1b --- /dev/null +++ b/Loop/Luminare/Settings/Keybindings/CustomActionConfigurationView.swift @@ -0,0 +1,343 @@ +// +// CustomActionConfigurationView.swift +// Loop +// +// Created by Kai Azim on 2024-04-27. +// + +import Defaults +import Luminare +import SwiftUI + +struct CustomActionConfigurationView: View { + @Binding var windowAction: WindowAction + @Binding var isPresented: Bool + + @State var action: WindowAction // this is so that onChange is called for each property + + @State private var currentTab: Tab = .position + private enum Tab: CaseIterable { + case position, size + + var name: String { + switch self { + case .position: + "Position" + case .size: + "Size" + } + } + + var image: Image { + switch self { + case .position: + Image(._18PxTableRows3Cols3) + case .size: + Image(._18PxSize) + } + } + } + + let anchors: [CustomWindowActionAnchor] = [ + .topLeft, .top, .topRight, + .left, .center, .right, + .bottomLeft, .bottom, .bottomRight + ] + + let previewController = PreviewController() + let screenSize: CGSize + + init(action: Binding, isPresented: Binding) { + self._windowAction = action + self._isPresented = isPresented + self._action = State(initialValue: action.wrappedValue) + + self.screenSize = NSScreen.main?.frame.size ?? NSScreen.screens[0].frame.size + } + + var body: some View { + ScreenView { + GeometryReader { geo in + let frame = action.getFrame( + window: nil, + bounds: .init(origin: .zero, size: geo.size), + isPreview: true + ) + + ZStack { + if action.sizeMode == .custom { + blurredWindow() + .frame( + width: frame.width, + height: frame.height + ) + .offset( + x: frame.origin.x, + y: frame.origin.y + ) + } + } + .frame(width: geo.size.width, height: geo.size.height, alignment: .topLeading) + .animation(.smooth(duration: 0.25), value: frame) + } + } + + LuminareSection { + LuminareTextField( + Binding( + get: { + action.name ?? "" + }, + set: { + action.name = $0 + } + ), + placeHolder: "Custom Keybind" + ) + } + + LuminareSection { + LuminarePicker( + elements: Tab.allCases, + selection: Binding( + get: { + currentTab + }, + set: { newValue in + withAnimation(.smooth(duration: 0.25)) { + currentTab = newValue + } + } + ), + columns: 2, + roundBottom: false + ) { tab in + HStack(spacing: 6) { + tab.image + Text(tab.name) + } + .fixedSize() + } + + LuminareToggle( + "Use pixels", + isOn: Binding( + get: { + if action.unit == nil { + action.unit = .percentage + } + return action.unit == .pixels + }, + set: { newValue in + withAnimation(.smooth(duration: 0.25)) { + if action.unit == .percentage { + action.width = min(action.width ?? 100, 100) + action.height = min(action.height ?? 100, 100) + + action.xPoint = min(action.xPoint ?? 100, 100) + action.yPoint = min(action.yPoint ?? 100, 100) + } + + action.unit = newValue ? .pixels : .percentage + } + } + ) + ) + } + + if currentTab == .position { + positionConfiguration() + } else if currentTab == .size { + sizeConfiguration() + } + + HStack(spacing: 8) { + Button("Preview") {} + .onLongPressGesture( + minimumDuration: 100.0, + maximumDistance: .infinity, + pressing: { pressing in + if pressing { + guard let screen = NSScreen.main else { return } + previewController.open(screen: screen, startingAction: action) + } else { + previewController.close() + } + }, + perform: {} + ) + .disabled(action.sizeMode != .custom) + + Button("Close") { + isPresented = false + } + } + .buttonStyle(LuminareCompactButtonStyle()) + .onChange(of: action) { _ in + windowAction = action + } + } + + @ViewBuilder func positionConfiguration() -> some View { + LuminareSection { + LuminareToggle( + "Use coordinates", + isOn: Binding( + get: { + action.positionMode == .coordinates + }, + set: { newValue in + withAnimation(.smooth(duration: 0.25)) { + action.positionMode = newValue ? .coordinates : .generic + } + } + ) + ) + + if action.positionMode ?? .generic == .generic { + LuminarePicker( + elements: anchors, + selection: Binding( + get: { + // since center/macOS center use the same icon on the picker + if action.anchor == .macOSCenter { + return .center + } + + return action.anchor ?? .center + }, + set: { newValue in + withAnimation(.smooth(duration: 0.25)) { + action.anchor = newValue + } + } + ), + columns: 3, + roundTop: false + ) { anchor in + IconView(action: .constant(anchor.iconAction)) + } + + if action.anchor ?? .center == .center || action.anchor == .macOSCenter { + LuminareToggle( + "Use macOS center", + isOn: Binding( + get: { + action.anchor == .macOSCenter + }, + set: { + action.anchor = $0 ? .macOSCenter : .center + } + ) + ) + } + } else { + LuminareValueAdjuster( + "X", + value: Binding( + get: { + action.xPoint ?? 0 + }, + set: { + action.xPoint = $0 + } + ), + sliderRange: action.unit == .percentage ? + 0...100 : + 0...Double(screenSize.width), + suffix: action.unit?.suffix ?? "", + lowerClamp: true + ) + + LuminareValueAdjuster( + "Y", + value: Binding( + get: { + action.yPoint ?? 0 + }, + set: { + action.yPoint = $0 + } + ), + sliderRange: action.unit == .percentage ? + 0...100 : + 0...Double(screenSize.height), + suffix: action.unit?.suffix ?? "", + lowerClamp: true + ) + } + } + } + + @ViewBuilder func sizeConfiguration() -> some View { + LuminareSection { + LuminarePicker( + elements: CustomWindowActionSizeMode.allCases, + selection: Binding( + get: { + action.sizeMode ?? .custom + }, + set: { newValue in + withAnimation(.smooth(duration: 0.25)) { + action.sizeMode = newValue + } + } + ), + columns: 3, + roundBottom: action.sizeMode != .custom + ) { mode in + VStack(spacing: 4) { + mode.image + Text(mode.name) + } + .padding(.vertical, 15) + } + + if action.sizeMode ?? .custom == .custom { + LuminareValueAdjuster( + "Width", + value: Binding( + get: { + action.width ?? 100 + }, + set: { + action.width = $0 + } + ), + sliderRange: action.unit == .percentage ? + 0...100 : + 0...Double(screenSize.width), + suffix: action.unit?.suffix ?? "", + lowerClamp: true + ) + + LuminareValueAdjuster( + "Height", + value: Binding( + get: { + action.height ?? 100 + }, + set: { + action.height = $0 + } + ), + sliderRange: action.unit == .percentage ? + 0...100 : + 0...Double(screenSize.width), + suffix: action.unit?.suffix ?? "", + lowerClamp: true + ) + } + } + } + + @ViewBuilder + func blurredWindow() -> some View { + VisualEffectView(material: .hudWindow, blendingMode: .withinWindow) + .overlay { + RoundedRectangle(cornerRadius: 5) + .strokeBorder(.white.opacity(0.1), lineWidth: 2) + } + .clipShape(.rect(cornerRadius: 5)) + } +} diff --git a/Loop/Luminare/Settings/Keybindings/CycleActionConfigurationView.swift b/Loop/Luminare/Settings/Keybindings/CycleActionConfigurationView.swift new file mode 100644 index 00000000..111dd61c --- /dev/null +++ b/Loop/Luminare/Settings/Keybindings/CycleActionConfigurationView.swift @@ -0,0 +1,93 @@ +// +// CycleActionConfigurationView.swift +// Loop +// +// Created by Kai Azim on 2024-05-03. +// + +import Defaults +import Luminare +import SwiftUI + +struct CycleActionConfigurationView: View { + @Binding var windowAction: WindowAction + @Binding var isPresented: Bool + + @State private var action: WindowAction // this is so that onChange is called for each property + + @State private var selectedKeybinds = Set() + + init(action: Binding, isPresented: Binding) { + self._windowAction = action + self._isPresented = isPresented + self._action = State(initialValue: action.wrappedValue) + } + + var body: some View { + LuminareSection { + LuminareTextField( + Binding( + get: { + action.name ?? "" + }, + set: { + action.name = $0 + } + ), + placeHolder: "Cycle Keybind" + ) + } + + LuminareList( + items: Binding( + get: { + if action.cycle == nil { + action.cycle = [] + } + + return action.cycle ?? [] + }, set: { newValue in + action.cycle = newValue + } + ), + selection: $selectedKeybinds, + addAction: { + if action.cycle == nil { + action.cycle = [] + } + + action.cycle?.insert(.init(.noAction), at: 0) + }, + content: { item in + KeybindingItemView( + item, + cycleIndex: action.cycle?.firstIndex(of: item.wrappedValue) + ) + .environmentObject(KeybindingsConfigurationModel()) + }, + emptyView: { + HStack { + Spacer() + VStack { + Text("Nothing to cycle through") + .font(.title3) + Text("Press \"Add\" to add a cycle item") + .font(.caption) + } + Spacer() + } + .foregroundStyle(.secondary) + .padding() + }, + id: \.id + ) + .onChange(of: action) { _ in + windowAction = action + } + + Button("Close") { + isPresented = false + } + .buttonStyle(LuminareCompactButtonStyle()) + } +} diff --git a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift new file mode 100644 index 00000000..dbd4a065 --- /dev/null +++ b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/Keycorder.swift @@ -0,0 +1,180 @@ +// +// Keycorder.swift +// Loop +// +// Created by Kai Azim on 2023-11-10. +// + +import Carbon.HIToolbox +import Defaults +import Luminare +import SwiftUI + +struct Keycorder: View { + @EnvironmentObject private var model: KeybindingsConfigurationModel + + let keyLimit: Int = 6 + + @Default(.triggerKey) var triggerKey + + @Binding private var validCurrentKeybind: Set + @State private var selectionKeybind: Set + @Binding private var direction: WindowDirection + + @State private var eventMonitor: NSEventMonitor? + @State private var shouldShake: Bool = false + @State private var shouldError: Bool = false + @State private var errorMessage: Text = .init("") // We use Text here for String interpolation with images + + @State private var isHovering: Bool = false + @State private var isActive: Bool = false + + init(_ keybind: Binding) { + self._validCurrentKeybind = keybind.keybind + self._direction = keybind.direction + self._selectionKeybind = State(initialValue: keybind.wrappedValue.keybind) + } + + var body: some View { + Button { + guard !isActive else { return } + startObservingKeys() + } label: { + if selectionKeybind.isEmpty { + Text(isActive ? "\(Image(systemName: "ellipsis"))" : "\(Image(systemName: "exclamationmark.triangle"))") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .fixedSize(horizontal: true, vertical: false) + .frame(width: 27, height: 27) + .font(.callout) + .modifier(LuminareBordered()) + } else { + HStack(spacing: 5) { + ForEach(selectionKeybind.sorted(), id: \.self) { key in + if let systemImage = key.systemImage { + Text("\(Image(systemName: systemImage))") + } else if let humanReadable = key.humanReadable { + Text(humanReadable) + } + } + .frame(width: 27, height: 27) + .font(.callout) + .modifier(LuminareBordered(highlight: $isHovering)) + } + } + } + .modifier(ShakeEffect(shakes: shouldShake ? 2 : 0)) + .animation(Animation.default, value: shouldShake) + .popover(isPresented: $shouldError, arrowEdge: .bottom) { + errorMessage + .multilineTextAlignment(.center) + .padding(8) + } + .onHover { hovering in + isHovering = hovering + } + .onChange(of: model.currentEventMonitor) { _ in + if model.currentEventMonitor != eventMonitor { + finishedObservingKeys(wasForced: true) + } + } + .onChange(of: validCurrentKeybind) { _ in + if selectionKeybind != validCurrentKeybind { + selectionKeybind = validCurrentKeybind + } + } + .buttonStyle(PlainButtonStyle()) + + // Don't allow the button to be pressed if more than one keybind is selected in the list + .allowsHitTesting(model.selectedKeybinds.count <= 1) + } + + func startObservingKeys() { + selectionKeybind = [] + isActive = true + eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .keyUp, .flagsChanged]) { event in + if event.type == .flagsChanged { + if !Defaults[.triggerKey].contains(where: { $0.baseModifier == event.keyCode.baseModifier }) { + shouldError = false + selectionKeybind.insert(event.keyCode.baseModifier) + } else { + if let systemImage = event.keyCode.baseModifier.systemImage { + errorMessage = Text("\(Image(systemName: systemImage)) is already used as your trigger key.") + } else { + errorMessage = Text("That key is already used as your trigger key.") + } + + shouldShake.toggle() + shouldError = true + } + } + + if event.type == .keyUp || + (event.type == .flagsChanged && !selectionKeybind.isEmpty && event.modifierFlags.rawValue == 256) { + finishedObservingKeys() + return + } + + if event.type == .keyDown, !event.isARepeat { + if event.keyCode == CGKeyCode.kVK_Escape { + finishedObservingKeys(wasForced: true) + return + } + + if (selectionKeybind.count + triggerKey.count) >= keyLimit { + errorMessage = Text( + "You can only use up to \(keyLimit) keys in a keybind, including the trigger key." + ) + shouldShake.toggle() + shouldError = true + } else { + shouldError = false + selectionKeybind.insert(event.keyCode) + } + } + } + + eventMonitor!.start() + model.currentEventMonitor = eventMonitor + } + + func finishedObservingKeys(wasForced: Bool = false) { + isActive = false + var willSet = !wasForced + + if validCurrentKeybind == selectionKeybind { + willSet = false + } + + if willSet { + for keybind in Defaults[.keybinds] where + keybind.keybind == selectionKeybind { + willSet = false + if keybind.direction == .custom { + if let name = keybind.name { + self.errorMessage = Text("That keybind is already being used by \(name).") + } else { + self.errorMessage = Text("That keybind is already being used by another custom keybind.") + } + } else { + self.errorMessage = Text( + "That keybind is already being used by \(keybind.direction.name.lowercased())." + ) + } + self.shouldShake.toggle() + self.shouldError = true + break + } + } + + if willSet { + // Set the valid keybind to the current selected one + validCurrentKeybind = selectionKeybind + } else { + // Set preview keybind back to previous one + selectionKeybind = validCurrentKeybind + } + + eventMonitor?.stop() + eventMonitor = nil + } +} diff --git a/Loop/Luminare/Settings/Keybindings/Keybind Recorder/TriggerKeycorder.swift b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/TriggerKeycorder.swift new file mode 100644 index 00000000..c4919c06 --- /dev/null +++ b/Loop/Luminare/Settings/Keybindings/Keybind Recorder/TriggerKeycorder.swift @@ -0,0 +1,155 @@ +// +// TriggerKeycorder.swift +// Loop +// +// Created by Kai Azim on 2023-09-11. +// + +import Defaults +import Luminare +import SwiftUI + +struct TriggerKeycorder: View { + @EnvironmentObject private var model: KeybindingsConfigurationModel + + let keyLimit: Int = 5 + + @Binding private var validCurrentKey: Set + @State private var selectionKey: Set + + @State private var eventMonitor: NSEventMonitor? + @State private var shouldShake: Bool = false + @State private var isHovering: Bool = false + @State private var isActive: Bool = false + @State private var tooManyKeysPopup: Bool = false + + init(_ key: Binding>) { + self._validCurrentKey = key + _selectionKey = State(initialValue: key.wrappedValue) + } + + var body: some View { + HStack { + Button { + guard !isActive else { return } + startObservingKeys() + } label: { + if selectionKey.isEmpty { + Text(isActive ? "Set a trigger key…" : "None") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .fixedSize(horizontal: true, vertical: false) + } else { + HStack(spacing: 12) { + ForEach(selectionKey.sorted(), id: \.self) { key in + Text("\(key.isOnRightSide ? String(localized: .init("Right", defaultValue: "Right")) : String(localized: .init("Left", defaultValue: "Left"))) \(Image(systemName: key.systemImage ?? "exclamationmark.circle.fill"))") + .frame(maxWidth: .infinity, maxHeight: .infinity) + .fixedSize(horizontal: true, vertical: false) + + if key != selectionKey.sorted().last { + Divider() + .padding(1) + } + } + } + } + } + .modifier(ShakeEffect(shakes: shouldShake ? 2 : 0)) + .animation(Animation.default, value: shouldShake) + .popover(isPresented: $tooManyKeysPopup, arrowEdge: .bottom) { + Text("You can only use up to \(keyLimit) keys in your trigger key.") + .multilineTextAlignment(.center) + .padding(8) + } + .onHover { hovering in + isHovering = hovering + } + .onChange(of: model.currentEventMonitor) { _ in + if model.currentEventMonitor != eventMonitor { + finishedObservingKeys(wasForced: true) + } + } + .onChange(of: validCurrentKey) { _ in + if selectionKey != validCurrentKey { + selectionKey = validCurrentKey + } + } + + .fixedSize() + .buttonStyle(LuminareCompactButtonStyle()) + + Spacer() + + Button("Change") { + guard !isActive else { return } + startObservingKeys() + } + .buttonStyle(LuminareCompactButtonStyle()) + .fixedSize() + } + } + + func startObservingKeys() { + selectionKey = [] + isActive = true + + // So that if doesn't interfere with the key detection here + AppDelegate.loopManager.setFlagsObservers(scope: .global) + + eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .flagsChanged]) { event in + // keyDown event is only used to track escape key + if event.type == .keyDown, event.keyCode == CGKeyCode.kVK_Escape { + finishedObservingKeys(wasForced: true) + } + + if CGKeyCode.keyToImage.contains(where: { $0.key == event.keyCode.baseModifier }) { + selectionKey.insert(event.keyCode) + } + + // Backup system in case keys are pressed at the exact same time + let flags = event.modifierFlags.convertToCGKeyCode() + if flags.count != selectionKey.count { + for key in flags where CGKeyCode.keyToImage.contains(where: { $0.key == key }) { + if !self.selectionKey.map(\.baseModifier).contains(key) { + self.selectionKey.insert(key) + } + } + } + + if event.modifierFlags.wasKeyUp, !selectionKey.isEmpty { + finishedObservingKeys() + return + } + + if !event.modifierFlags.wasKeyUp, selectionKey.isEmpty { + shouldShake.toggle() + } + } + + eventMonitor!.start() + model.currentEventMonitor = eventMonitor + } + + func finishedObservingKeys(wasForced: Bool = false) { + var willSet = !wasForced + + if selectionKey.count > keyLimit { + willSet = false + shouldShake.toggle() + tooManyKeysPopup = true + } + + isActive = false + + if willSet { + // Set the valid keybind to the current selected one + validCurrentKey = selectionKey + } else { + // Set preview keybind back to previous one + selectionKey = validCurrentKey + } + + eventMonitor?.stop() + eventMonitor = nil + AppDelegate.loopManager.setFlagsObservers(scope: .all) + } +} diff --git a/Loop/Luminare/Settings/Keybindings/KeybindingItem.swift b/Loop/Luminare/Settings/Keybindings/KeybindingItem.swift new file mode 100644 index 00000000..26ff23a3 --- /dev/null +++ b/Loop/Luminare/Settings/Keybindings/KeybindingItem.swift @@ -0,0 +1,227 @@ +// +// KeybindingItem.swift +// Loop +// +// Created by Kai Azim on 2024-05-03. +// + +import Defaults +import Luminare +import SwiftUI + +struct KeybindingItemView: View { + @Environment(\.hoveringOverLuminareListItem) var isHovering + + @Default(.triggerKey) var triggerKey + @Binding var keybind: WindowAction + + @State var isConfiguringCustom: Bool = false + @State var isConfiguringCycle: Bool = false + + let cycleIndex: Int? + + init(_ keybind: Binding, cycleIndex: Int? = nil) { + self._keybind = keybind + self.cycleIndex = cycleIndex + } + + var body: some View { + HStack { + label() + .onChange(of: keybind) { _ in + if keybind.direction == .custom { + isConfiguringCustom = true + } + if keybind.direction == .cycle { + isConfiguringCycle = true + } + } + + HStack { + if keybind.direction == .custom { + Button(action: { + isConfiguringCustom = true + }, label: { + Image(._18PxRuler) + }) + .buttonStyle(.plain) + .luminareModal(isPresented: $isConfiguringCustom) { + CustomActionConfigurationView(action: $keybind, isPresented: $isConfiguringCustom) + .frame(width: 400) + } + } + + if keybind.direction == .cycle { + Button(action: { + isConfiguringCycle = true + }, label: { + Image(._18PxRepeat4) + }) + .buttonStyle(.plain) + .luminareModal(isPresented: $isConfiguringCycle) { + CycleActionConfigurationView(action: $keybind, isPresented: $isConfiguringCycle) + .frame(width: 400) + } + } + + if isHovering { + WindowDirectionPicker($keybind, isCycle: cycleIndex != nil) + .equatable() + } + } + .font(.title3) + .foregroundStyle(isHovering ? .primary : .secondary) + + Spacer() + + if let cycleIndex { + Text("\(cycleIndex)") + .frame(width: 27, height: 27) + .modifier(LuminareBordered()) + } else { + HStack(spacing: 6) { + HStack { + ForEach(triggerKey.sorted().compactMap(\.systemImage), id: \.self) { image in + Text("\(Image(systemName: image))") + } + } + .font(.callout) + .padding(6) + .frame(height: 27) + .modifier(LuminareBordered()) + + Image(systemName: "plus") + + Keycorder($keybind) + } + .fixedSize() + } + } + .padding(.horizontal, 12) + } + + func label() -> some View { + HStack(spacing: 0) { + HStack(spacing: 8) { + IconView(action: $keybind) + + Text(keybind.getName()) + .lineLimit(1) + } + + if let info = keybind.direction.infoView { + info + } + } + .fixedSize(horizontal: false, vertical: true) + } +} + +struct WindowDirectionPicker: View, Equatable { + @Binding var keybind: WindowAction + let isCycle: Bool + + init(_ keybind: Binding, isCycle: Bool = false) { + self._keybind = keybind + self.isCycle = isCycle + } + + var body: some View { + Menu { + Menu("General") { + ForEach(WindowDirection.general) { direction in + directionPickerItem(direction) + } + } + + Menu("Halves") { + ForEach(WindowDirection.halves) { direction in + directionPickerItem(direction) + } + } + + Menu("Quarters") { + ForEach(WindowDirection.quarters) { direction in + directionPickerItem(direction) + } + } + + Menu("Horizontal Thirds") { + ForEach(WindowDirection.horizontalThirds) { direction in + directionPickerItem(direction) + } + } + + Menu("Vertical Thirds") { + ForEach(WindowDirection.verticalThirds) { direction in + directionPickerItem(direction) + } + } + + Menu("Screen Switching") { + ForEach(WindowDirection.screenSwitching) { direction in + directionPickerItem(direction) + } + } + + if !isCycle { + Menu("Grow/Shrink") { + ForEach(WindowDirection.sizeAdjustment) { direction in + directionPickerItem(direction) + } + Divider() + ForEach(WindowDirection.shrink) { direction in + directionPickerItem(direction) + } + Divider() + ForEach(WindowDirection.grow) { direction in + directionPickerItem(direction) + } + } + } + + Menu("More") { + ForEach(WindowDirection.more) { direction in + if isCycle { + if direction != .cycle { + directionPickerItem(direction) + } + } else { + directionPickerItem(direction) + } + } + } + } label: { + Image(._18PxPen2) + .padding(.vertical, 5) // Increase hitbox size + .contentShape(.rect) + .padding(.vertical, -5) // So that the picker dropdown doesn't get offsetted by the hitbox + } + .buttonStyle(PlainButtonStyle()) // Override Luminare button styling + } + + func directionPickerItem(_ direction: WindowDirection) -> some View { + Button(action: { + keybind.direction = direction + + if direction == .custom { + keybind.unit = .percentage + keybind.anchor = .center + keybind.sizeMode = .custom + keybind.width = 80 + keybind.height = 80 + keybind.positionMode = .generic + keybind.xPoint = 10 + keybind.yPoint = 10 + } + }, label: { + HStack { + Text(direction.name) + } + }) + } + + static func == (lhs: WindowDirectionPicker, rhs: WindowDirectionPicker) -> Bool { + lhs.keybind == rhs.keybind + } +} diff --git a/Loop/Luminare/Settings/Keybindings/KeybindingsConfiguration.swift b/Loop/Luminare/Settings/Keybindings/KeybindingsConfiguration.swift new file mode 100644 index 00000000..2531fd47 --- /dev/null +++ b/Loop/Luminare/Settings/Keybindings/KeybindingsConfiguration.swift @@ -0,0 +1,100 @@ +// +// KeybindingsConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-20. +// + +import Defaults +import Luminare +import SwiftUI + +class KeybindingsConfigurationModel: ObservableObject { + @Published var triggerKey = Defaults[.triggerKey] { + didSet { + Defaults[.triggerKey] = triggerKey + } + } + + @Published var triggerDelay = Defaults[.triggerDelay] { + didSet { + Defaults[.triggerDelay] = triggerDelay + } + } + + @Published var doubleClickToTrigger = Defaults[.doubleClickToTrigger] { + didSet { + Defaults[.doubleClickToTrigger] = doubleClickToTrigger + } + } + + @Published var middleClickTriggersLoop = Defaults[.middleClickTriggersLoop] { + didSet { + Defaults[.middleClickTriggersLoop] = middleClickTriggersLoop + } + } + + @Published var keybinds = Defaults[.keybinds] { + didSet { + Defaults[.keybinds] = keybinds + } + } + + @Published var currentEventMonitor: NSEventMonitor? + @Published var selectedKeybinds = Set() +} + +struct KeybindingsConfigurationView: View { + @StateObject private var model = KeybindingsConfigurationModel() + + var body: some View { + LuminareSection("Trigger Key", noBorder: true) { + // TODO: Make long trigger keys fit in bounds + TriggerKeycorder($model.triggerKey) + .environmentObject(model) + } + + LuminareSection("Settings") { + LuminareValueAdjuster( + "Trigger delay", + value: $model.triggerDelay, + sliderRange: 0...1, + suffix: .init(.init(localized: "Seconds", defaultValue: "s")), + step: 0.1, + lowerClamp: true, + decimalPlaces: 1 + ) + + LuminareToggle("Double-click to trigger", isOn: $model.doubleClickToTrigger) + LuminareToggle("Middle-click to trigger", isOn: $model.middleClickTriggersLoop) + } + + LuminareList( + "Keybinds", + items: $model.keybinds, + selection: $model.selectedKeybinds, + addAction: { + model.keybinds.insert(.init(.noAction), at: 0) + }, + content: { keybind in + KeybindingItemView(keybind) + .environmentObject(model) + }, + emptyView: { + HStack { + Spacer() + VStack { + Text("No keybinds") + .font(.title3) + Text("Press \"Add\" to add a keybind") + .font(.caption) + } + Spacer() + } + .foregroundStyle(.secondary) + .padding() + }, + id: \.id + ) + } +} diff --git a/Loop/Luminare/Theming/AccentColorConfiguration.swift b/Loop/Luminare/Theming/AccentColorConfiguration.swift new file mode 100644 index 00000000..464ae78b --- /dev/null +++ b/Loop/Luminare/Theming/AccentColorConfiguration.swift @@ -0,0 +1,89 @@ +// +// AccentColorConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-19. +// + +import Defaults +import Luminare +import SwiftUI + +class AccentColorConfigurationModel: ObservableObject { + @Published var useSystemAccentColor = Defaults[.useSystemAccentColor] { + didSet { + Defaults[.useSystemAccentColor] = useSystemAccentColor + } + } + + @Published var useGradient = Defaults[.useGradient] { + didSet { + Defaults[.useGradient] = useGradient + } + } + + @Published var customAccentColor = Defaults[.customAccentColor] { + didSet { + Defaults[.customAccentColor] = customAccentColor + } + } + + @Published var gradientColor = Defaults[.gradientColor] { + didSet { + Defaults[.gradientColor] = gradientColor + } + } +} + +struct AccentColorConfigurationView: View { + @StateObject private var model = AccentColorConfigurationModel() + + var body: some View { + LuminareSection { + LuminarePicker( + elements: [true, false], + selection: $model.useSystemAccentColor.animation(.smooth(duration: 0.25)), + columns: 2, + roundBottom: false + ) { item in + VStack { + Spacer() + Spacer() + + if item { + Image(systemName: "apple.logo") + } else { + Image(._18PxColorPalette) + } + + Spacer() + + Text(item ? "System" : "Custom") + + Spacer() + Spacer() + } + .font(.title3) + .frame(height: 90) + } + + LuminareToggle("Gradient", isOn: $model.useGradient.animation(.smooth(duration: 0.25))) + } + + VStack { + if !model.useSystemAccentColor || (model.useGradient && !model.useSystemAccentColor) { + HStack { + Text("Color") + Spacer() + } + .foregroundStyle(.secondary) + + LuminareColorPicker(color: $model.customAccentColor) + + if model.useGradient { + LuminareColorPicker(color: $model.gradientColor) + } + } + } + } +} diff --git a/Loop/Luminare/Theming/IconConfiguration.swift b/Loop/Luminare/Theming/IconConfiguration.swift new file mode 100644 index 00000000..9a93a5da --- /dev/null +++ b/Loop/Luminare/Theming/IconConfiguration.swift @@ -0,0 +1,123 @@ +// +// IconConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-19. +// + +import Defaults +import Luminare +import SwiftUI + +class IconConfigurationModel: ObservableObject { + let suggestNewIconLink = URL(string: "https://github.com/MrKai77/Loop/issues/new/choose")! + + @Published var currentIcon = Defaults[.currentIcon] { + didSet { + Defaults[.currentIcon] = currentIcon + + DispatchQueue.main.async { + IconManager.refreshCurrentAppIcon() + } + } + } + + @Published var showDockIcon = Defaults[.showDockIcon] { + didSet { + Defaults[.showDockIcon] = showDockIcon + } + } + + @Published var notificationWhenIconUnlocked = Defaults[.notificationWhenIconUnlocked] { + didSet { + Defaults[.notificationWhenIconUnlocked] = notificationWhenIconUnlocked + + if notificationWhenIconUnlocked { + let notficationBody: String = .init( + localized: .init( + "Default notification content", + defaultValue: "You will now be notified when you unlock a new icon." + ) + ) + AppDelegate.sendNotification(Bundle.main.appName, notficationBody) + + let areNotificationsEnabled = AppDelegate.areNotificationsEnabled() + + if !areNotificationsEnabled { + notificationWhenIconUnlocked = false + userDisabledNotificationsAlert() + } + } + } + } + + private func userDisabledNotificationsAlert() { + guard + let window = LuminareManager.window + else { + return + } + let alert = NSAlert() + alert.messageText = "\(Bundle.main.appName)'s notification permissions are currently disabled." + alert.informativeText = "Please turn them on in System Settings." + alert.addButton(withTitle: "Open Settings") + alert.alertStyle = .warning + + alert.beginSheetModal(for: window) { modalResponse in + if modalResponse == .alertFirstButtonReturn { + NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension")! + ) + } + } + } +} + +struct IconConfigurationView: View { + @Environment(\.openURL) var openURL + @StateObject private var model = IconConfigurationModel() + + var body: some View { + LuminareSection(showDividers: false) { + LuminarePicker( + elements: Icon.all, + selection: Binding( + get: { + IconManager.currentAppIcon + }, + set: { + model.currentIcon = $0.iconName + } + ), + roundBottom: false + ) { icon in + Group { + if icon.selectable { + Image(nsImage: NSImage(named: icon.iconName)!) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(10) + } else { + VStack(alignment: .center) { + Spacer() + + Image(._18PxLock) + + Spacer() + } + } + } + .aspectRatio(1, contentMode: .fit) + } + + Button("Suggest new icon") { + openURL(model.suggestNewIconLink) + } + } + + LuminareSection("Options") { + LuminareToggle("Show in dock", isOn: $model.showDockIcon) + LuminareToggle("Notify when unlocking new icons", isOn: $model.notificationWhenIconUnlocked) + } + } +} diff --git a/Loop/Luminare/Theming/PreviewConfiguration.swift b/Loop/Luminare/Theming/PreviewConfiguration.swift new file mode 100644 index 00000000..9715ff9b --- /dev/null +++ b/Loop/Luminare/Theming/PreviewConfiguration.swift @@ -0,0 +1,78 @@ +// +// PreviewConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-19. +// + +import Defaults +import Luminare +import SwiftUI + +class PreviewConfigurationModel: ObservableObject { + @Published var previewVisibility = Defaults[.previewVisibility] { + didSet { + Defaults[.previewVisibility] = previewVisibility + + // We can't move the cursor with the window if the window is going to be moving everywhere + if !previewVisibility { + Defaults[.moveCursorWithWindow] = false + } + } + } + + @Published var previewPadding = Defaults[.previewPadding] { + didSet { + Defaults[.previewPadding] = previewPadding + } + } + + @Published var previewCornerRadius = Defaults[.previewCornerRadius] { + didSet { + Defaults[.previewCornerRadius] = previewCornerRadius + } + } + + @Published var previewBorderThickness = Defaults[.previewBorderThickness] { + didSet { + Defaults[.previewBorderThickness] = previewBorderThickness + } + } +} + +struct PreviewConfigurationView: View { + @StateObject private var model = PreviewConfigurationModel() + + var body: some View { + LuminareSection { + LuminareToggle("Show preview when looping", isOn: $model.previewVisibility) + + LuminareValueAdjuster( + "Padding", + value: $model.previewPadding, + sliderRange: 0...20, + suffix: "px", + lowerClamp: true, + upperClamp: true + ) + + LuminareValueAdjuster( + "Corner radius", + value: $model.previewCornerRadius, + sliderRange: 0...20, + suffix: "px", + lowerClamp: true, + upperClamp: true + ) + + LuminareValueAdjuster( + "Border thickness", + value: $model.previewBorderThickness, + sliderRange: 0...10, + suffix: "px", + lowerClamp: true, + upperClamp: true + ) + } + } +} diff --git a/Loop/Luminare/Theming/RadialMenuConfiguration.swift b/Loop/Luminare/Theming/RadialMenuConfiguration.swift new file mode 100644 index 00000000..7eaa0dba --- /dev/null +++ b/Loop/Luminare/Theming/RadialMenuConfiguration.swift @@ -0,0 +1,73 @@ +// +// RadialMenuConfiguration.swift +// Loop +// +// Created by Kai Azim on 2024-04-19. +// + +import Defaults +import Luminare +import SwiftUI + +class RadialMenuConfigurationModel: ObservableObject { + @Published var radialMenuVisibility = Defaults[.radialMenuVisibility] { + didSet { + Defaults[.radialMenuVisibility] = radialMenuVisibility + } + } + + @Published var disableCursorInteraction = Defaults[.disableCursorInteraction] { + didSet { + Defaults[.disableCursorInteraction] = disableCursorInteraction + } + } + + @Published var radialMenuCornerRadius = Defaults[.radialMenuCornerRadius] { + didSet { + Defaults[.radialMenuCornerRadius] = radialMenuCornerRadius + + if radialMenuCornerRadius - 1 < radialMenuThickness { + radialMenuThickness = radialMenuCornerRadius - 1 + } + } + } + + @Published var radialMenuThickness = Defaults[.radialMenuThickness] { + didSet { + Defaults[.radialMenuThickness] = radialMenuThickness + + if radialMenuThickness + 1 > radialMenuCornerRadius { + radialMenuCornerRadius = radialMenuThickness + 1 + } + } + } +} + +struct RadialMenuConfigurationView: View { + @StateObject private var model = RadialMenuConfigurationModel() + + var body: some View { + LuminareSection { + LuminareToggle("Radial menu", isOn: $model.radialMenuVisibility) + LuminareToggle("Disable cursor interaction", isOn: $model.disableCursorInteraction) + + LuminareValueAdjuster( + "Corner radius", + value: $model.radialMenuCornerRadius, + sliderRange: 30...50, + suffix: "px", + lowerClamp: true, + upperClamp: true + ) + + LuminareValueAdjuster( + "Thickness", + value: $model.radialMenuThickness, + sliderRange: 10...35, + suffix: "px", + lowerClamp: true, + upperClamp: true + ) + } + } +} diff --git a/Loop/Managers/AppListManager.swift b/Loop/Managers/AppListManager.swift deleted file mode 100644 index 8bd69592..00000000 --- a/Loop/Managers/AppListManager.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// AppListManager.swift -// Loop -// -// Created by Dirk Mika on 11.03.24. -// - -import SwiftUI -import Algorithms - -class AppListManager: ObservableObject { - struct App: Identifiable { - var id: String { bundleID } - var bundleID: String - var icon: NSImage - var displayName: String - var installationFolder: String - } - - private var qry = NSMetadataQuery() - - @Published var installedApps = [App]() - - init() { - self.startQuery() - } - - deinit { - qry.stop() - } - - private func startQuery() { - qry.predicate = NSPredicate(format: "kMDItemContentType == 'com.apple.application-bundle'") - if let appFolder = FileManager.default.urls(for: .applicationDirectory, in: .localDomainMask).first { - qry.searchScopes = [appFolder] - } - - NotificationCenter.default.addObserver( - forName: .NSMetadataQueryDidFinishGathering, - object: nil, - queue: nil, - using: queryDidFinishGathering - ) - qry.start() - } - - private func queryDidFinishGathering(notification: Notification) { - if let items = qry.results as? [NSMetadataItem] { - self.installedApps = items.compactMap { item in - guard - let bundleId = item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String, - let displayName = item.value(forAttribute: NSMetadataItemDisplayNameKey) as? String, - let path = item.value(forAttribute: NSMetadataItemPathKey) as? String, - let installationFolder = URL(string: path)?.deletingLastPathComponent().path(percentEncoded: false) - else { - return nil - } - let icon = NSWorkspace.shared.icon(forFile: path) - return App( - bundleID: bundleId, - icon: icon, - displayName: displayName, - installationFolder: installationFolder - ) - } - } - } -} diff --git a/Loop/Managers/IconManager.swift b/Loop/Managers/IconManager.swift index 67403119..893950fe 100644 --- a/Loop/Managers/IconManager.swift +++ b/Loop/Managers/IconManager.swift @@ -5,134 +5,29 @@ // Created by Kai Azim on 2023-02-14. // -import SwiftUI import Defaults +import Luminare +import SwiftUI import UserNotifications class IconManager { - struct Icon: Hashable { - var name: String? - var iconName: String - var unlockTime: Int - var unlockMessage: String? - var footer: String? - - func getName() -> String { - if let name = self.name { - return name - } - - let prefix = "AppIcon-" - return iconName.replacingOccurrences(of: prefix, with: "") - } - - static var gregLassaleFooter = String( - localized: .init( - "Greg Lassale Footer", - defaultValue: "This icon was designed by Greg Lassale (@greglassale on 𝕏)" - ) - ) - } - - private static let icons: [Icon] = [ - Icon( - name: .init(localized: .init("Icon Name: Classic", defaultValue: "Classic")), - iconName: "AppIcon-Classic", - unlockTime: 0 - ), - Icon( - name: .init(localized: .init("Icon Name: Holo", defaultValue: "Holo")), - iconName: "AppIcon-Holo", - unlockTime: 25, - unlockMessage: .init( - localized: .init( - "Icon Unlock Message: Holo", - defaultValue: """ -You've already looped 25 times! As a reward, here's new icon: \(.init(localized: .init("Icon Name: Holo", defaultValue: "Holo"))). Continue to loop more to unlock new icons! -""" - ) - ) - ), - Icon( - name: .init(localized: .init("Icon Name: Rosé Pine", defaultValue: "Rosé Pine")), - iconName: "AppIcon-Rose Pine", - unlockTime: 50 - ), - Icon( - name: .init(localized: .init("Icon Name: Meta Loop", defaultValue: "Meta Loop")), - iconName: "AppIcon-Meta Loop", - unlockTime: 100 - ), - Icon( - name: .init(localized: .init("Icon Name: Keycap", defaultValue: "Keycap")), - iconName: "AppIcon-Keycap", - unlockTime: 200 - ), - Icon( - name: .init(localized: .init("Icon Name: White", defaultValue: "White")), - iconName: "AppIcon-White", - unlockTime: 400 - ), - Icon( - name: .init(localized: .init("Icon Name: Black", defaultValue: "Black")), - iconName: "AppIcon-Black", - unlockTime: 500 - ), - - Icon( - name: .init(localized: .init("Icon Name: Simon", defaultValue: "Simon")), - iconName: "AppIcon-Simon", - unlockTime: 1000, - footer: Icon.gregLassaleFooter - ), - Icon( - name: .init(localized: .init("Icon Name: Neon", defaultValue: "Neon")), - iconName: "AppIcon-Neon", - unlockTime: 1500, - footer: Icon.gregLassaleFooter - ), - Icon( - name: .init(localized: .init("Icon Name: Synthwave Sunset", defaultValue: "Synthwave Sunset")), - iconName: "AppIcon-Synthwave Sunset", - unlockTime: 2000, footer: Icon.gregLassaleFooter - ), - Icon( - name: .init(localized: .init("Icon Name: Black Hole", defaultValue: "Black Hole")), - iconName: "AppIcon-Black Hole", - unlockTime: 2500, footer: Icon.gregLassaleFooter - ), - - Icon( - name: .init(localized: .init("Icon Name: Loop Master", defaultValue: "Loop Master")), - iconName: "AppIcon-Loop Master", - unlockTime: 5000, - unlockMessage: .init( - localized: .init( - "Icon Unlock Message: Loop Master", - defaultValue: """ -5000 loops conquered! The universe has witnessed the birth of a Loop master! Enjoy your well-deserved reward: a brand-new icon! -""" - ) - ) - ) - ] - static func returnUnlockedIcons() -> [Icon] { var returnValue: [Icon] = [] - for icon in icons where icon.unlockTime <= Defaults[.timesLooped] { + for icon in Icon.all where icon.unlockTime <= Defaults[.timesLooped] { returnValue.append(icon) } + return returnValue.reversed() } static func setAppIcon(to icon: Icon) { Defaults[.currentIcon] = icon.iconName - self.refreshCurrentAppIcon() - print("Setting app icon to: \(icon.getName())") + refreshCurrentAppIcon() + print("Setting app icon to: \(icon.name)") } static func setAppIcon(to iconName: String) { - if let targetIcon = icons.first(where: { $0.iconName == iconName }) { + if let targetIcon = Icon.all.first(where: { $0.iconName == iconName }) { setAppIcon(to: targetIcon) } } @@ -146,7 +41,7 @@ You've already looped 25 times! As a reward, here's new icon: \(.init(localized: static func checkIfUnlockedNewIcon() { guard Defaults[.notificationWhenIconUnlocked] else { return } - for icon in icons where icon.unlockTime == Defaults[.timesLooped] { + for icon in Icon.all where icon.unlockTime == Defaults[.timesLooped] { let content = UNMutableNotificationContent() content.title = Bundle.main.appName @@ -157,7 +52,7 @@ You've already looped 25 times! As a reward, here's new icon: \(.init(localized: content.body = .init( localized: .init( "Icon Unlock Message", - defaultValue: "You've unlocked a new icon: \(icon.getName())!" + defaultValue: "You've unlocked a new icon: \(icon.name)!" ) ) } @@ -175,8 +70,8 @@ You've already looped 25 times! As a reward, here's new icon: \(.init(localized: } static var currentAppIcon: Icon { - return icons.first { + Icon.all.first { $0.iconName == Defaults[.currentIcon] - } ?? icons.first! + } ?? Icon.all.first! } } diff --git a/Loop/Managers/KeybindMonitor.swift b/Loop/Managers/KeybindMonitor.swift index 86b639e5..b7d68847 100644 --- a/Loop/Managers/KeybindMonitor.swift +++ b/Loop/Managers/KeybindMonitor.swift @@ -14,23 +14,23 @@ class KeybindMonitor { private var eventMonitor: CGEventMonitor? private var flagsEventMonitor: CGEventMonitor? private var pressedKeys = Set() - private var lastKeyReleaseTime: Date = Date.now + private var lastKeyReleaseTime: Date = .now // Currently, special events only contain the globe key, as it can also be used as a emoji key. private let specialEvents: [CGKeyCode] = [179] - var canPassthroughSpecialEvents = true // If mouse has been moved + var canPassthroughSpecialEvents = true // If mouse has been moved func resetPressedKeys() { KeybindMonitor.shared.pressedKeys = [] } func start() { - guard self.eventMonitor == nil, + guard eventMonitor == nil, AccessibilityManager.getStatus() else { return } - self.eventMonitor = CGEventMonitor(eventMask: [.keyDown, .keyUp]) { cgEvent in + eventMonitor = CGEventMonitor(eventMask: [.keyDown, .keyUp]) { cgEvent in guard cgEvent.type == .keyDown || cgEvent.type == .keyUp, let event = NSEvent(cgEvent: cgEvent) @@ -44,31 +44,31 @@ class KeybindMonitor { KeybindMonitor.shared.pressedKeys.insert(event.keyCode.baseKey) } - // Special events such as the emoji key - if self.specialEvents.contains(event.keyCode.baseKey) { - if self.canPassthroughSpecialEvents { - return Unmanaged.passUnretained(cgEvent) - } - return nil - } + // Special events such as the emoji key + if self.specialEvents.contains(event.keyCode.baseKey) { + if self.canPassthroughSpecialEvents { + return Unmanaged.passUnretained(cgEvent) + } + return nil + } // If this is a valid event, don't passthrough if self.performKeybind(event: event) { return nil } - // If this wasn't, check if it was a system keybind (ex. screenshot), and - // in that case, passthrough and foce-close Loop - if CGKeyCode.systemKeybinds.contains(self.pressedKeys) { - Notification.Name.forceCloseLoop.post() - print("Detected system keybind, closing!") - return Unmanaged.passUnretained(cgEvent) - } + // If this wasn't, check if it was a system keybind (ex. screenshot), and + // in that case, passthrough and foce-close Loop + if CGKeyCode.systemKeybinds.contains(self.pressedKeys) { + Notification.Name.forceCloseLoop.post() + print("Detected system keybind, closing!") + return Unmanaged.passUnretained(cgEvent) + } return Unmanaged.passUnretained(cgEvent) } - self.flagsEventMonitor = CGEventMonitor(eventMask: .flagsChanged) { cgEvent in + flagsEventMonitor = CGEventMonitor(eventMask: .flagsChanged) { cgEvent in if cgEvent.type == .flagsChanged, let event = NSEvent(cgEvent: cgEvent), !Defaults[.triggerKey].contains(where: { $0.baseModifier == event.keyCode.baseModifier }) { @@ -82,19 +82,19 @@ class KeybindMonitor { return Unmanaged.passUnretained(cgEvent) } - self.eventMonitor!.start() - self.flagsEventMonitor!.start() + eventMonitor!.start() + flagsEventMonitor!.start() } func stop() { - self.resetPressedKeys() - self.canPassthroughSpecialEvents = true + resetPressedKeys() + canPassthroughSpecialEvents = true - self.eventMonitor?.stop() - self.eventMonitor = nil + eventMonitor?.stop() + eventMonitor = nil - self.flagsEventMonitor?.stop() - self.flagsEventMonitor = nil + flagsEventMonitor?.stop() + flagsEventMonitor = nil } @discardableResult diff --git a/Loop/Managers/LoopManager.swift b/Loop/Managers/LoopManager.swift index 79f280ad..7c5d9377 100644 --- a/Loop/Managers/LoopManager.swift +++ b/Loop/Managers/LoopManager.swift @@ -5,10 +5,9 @@ // Created by Kai Azim on 2023-08-15. // -import SwiftUI import Defaults +import SwiftUI -// swiftlint:disable:next type_body_length class LoopManager: ObservableObject { // Size Adjustment static var sidesToAdjust: Edge.Set? @@ -27,13 +26,12 @@ class LoopManager: ObservableObject { private var flagsChangedEventMonitor: EventMonitor? private var mouseMovedEventMonitor: EventMonitor? - private var keyDownEventMonitor: EventMonitor? private var middleClickMonitor: EventMonitor? private var lastTriggerKeyClick: Date = .now @Published var currentAction: WindowAction = .init(.noAction) - private var initialMousePosition: CGPoint = CGPoint() - private var angleToMouse: Angle = Angle(degrees: 0) + private var initialMousePosition: CGPoint = .init() + private var angleToMouse: Angle = .init(degrees: 0) private var distanceToMouse: CGFloat = 0 private var triggerDelayTimer: Timer? { @@ -42,15 +40,19 @@ class LoopManager: ObservableObject { } } - func startObservingKeys() { - flagsChangedEventMonitor = NSEventMonitor( - scope: .global, - eventMask: .flagsChanged, - handler: handleLoopKeypress(_:) - ) + func start() { + Notification.Name.forceCloseLoop.onReceive { _ in + self.closeLoop(forceClose: true) + } + + Notification.Name.updateBackendDirection.onReceive { notification in + if let action = notification.userInfo?["action"] as? WindowAction { + self.changeAction(action) + } + } mouseMovedEventMonitor = NSEventMonitor( - scope: .global, + scope: .all, eventMask: [.mouseMoved, .otherMouseDragged], handler: mouseMoved(_:) ) @@ -60,32 +62,24 @@ class LoopManager: ObservableObject { callback: handleMiddleClick(cgEvent:) ) - keyDownEventMonitor = NSEventMonitor( - scope: .global, - eventMask: .keyDown - ) { _ in - if Defaults[.doubleClickToTrigger] && - abs(self.lastTriggerKeyClick.timeIntervalSinceNow) < NSEvent.doubleClickInterval { - self.lastTriggerKeyClick = Date.distantPast - } - } + setFlagsObservers(scope: .all) + middleClickMonitor?.start() + } - Notification.Name.forceCloseLoop.onReceive { _ in - self.closeLoop(forceClose: true) - } + // This is called when setting the trigger key, so that there aren't conflicting event monitors + func setFlagsObservers(scope: NSEventMonitor.Scope = .all) { + flagsChangedEventMonitor?.stop() - Notification.Name.updateBackendDirection.onReceive { notification in - if let action = notification.userInfo?["action"] as? WindowAction { - self.changeAction(action) - } - } + flagsChangedEventMonitor = NSEventMonitor( + scope: scope, + eventMask: .flagsChanged, + handler: handleLoopKeypress(_:) + ) - flagsChangedEventMonitor!.start() - middleClickMonitor!.start() - keyDownEventMonitor!.start() + flagsChangedEventMonitor?.start() } - private func mouseMoved(_ event: NSEvent) { + private func mouseMoved(_: NSEvent) { guard isLoopActive else { return } keybindMonitor.canPassthroughSpecialEvents = false @@ -96,7 +90,7 @@ class LoopManager: ObservableObject { let mouseDistance = initialMousePosition.distanceSquared(to: currentMouseLocation) // Return if the mouse didn't move - if (mouseAngle == angleToMouse) && (mouseDistance == distanceToMouse) { + if mouseAngle == angleToMouse, mouseDistance == distanceToMouse { return } @@ -149,34 +143,34 @@ class LoopManager: ObservableObject { } private func getNextCycleAction(_ action: WindowAction) -> WindowAction { - guard let cycle = action.cycle else { + guard let currentCycle = action.cycle else { return action } var nextIndex = 0 - if !cycle.contains(currentAction), + if !currentCycle.contains(currentAction), let window = targetWindow, let latestRecord = WindowRecords.getCurrentAction(for: window) { // We "preserve" the cycle index based on the last record - nextIndex = (cycle.firstIndex(of: latestRecord) ?? -1) + 1 + nextIndex = (currentCycle.firstIndex(of: latestRecord) ?? -1) + 1 } else if currentAction.direction == .custom { // We need to check if *all* the characteristics of the action are the same - nextIndex = (cycle.firstIndex(of: currentAction) ?? -1) + 1 + nextIndex = (currentCycle.firstIndex(of: currentAction) ?? -1) + 1 } else { // Only check the direction, since the rest of the info is insignificant - nextIndex = (cycle.firstIndex { $0.direction == currentAction.direction } ?? -1) + 1 + nextIndex = (currentCycle.firstIndex { $0.direction == currentAction.direction } ?? -1) + 1 } - if nextIndex >= cycle.count { + if nextIndex >= currentCycle.count { nextIndex = 0 } - return cycle[nextIndex] + return currentCycle[nextIndex] } - private func changeAction(_ action: WindowAction) { + private func changeAction(_ action: WindowAction, triggeredFromScreenChange: Bool = false) { guard currentAction != action || action.willManipulateCurrentWindowSize, isLoopActive, @@ -189,6 +183,12 @@ class LoopManager: ObservableObject { if newAction.direction == .cycle { newAction = getNextCycleAction(action) + + // Prevents an endless loop of cycling screens + if triggeredFromScreenChange, newAction.direction.willChangeScreen { + performHapticFeedback() + return + } } if newAction.direction.willChangeScreen { @@ -219,13 +219,14 @@ class LoopManager: ObservableObject { if action.direction == .cycle { currentAction = newAction - changeAction(action) + changeAction(action, triggeredFromScreenChange: true) } else { - if let screenToResizeOn = screenToResizeOn, + if let screenToResizeOn, + let window = targetWindow, !Defaults[.previewVisibility] { performHapticFeedback() WindowEngine.resize( - targetWindow!, + window, to: currentAction, on: screenToResizeOn ) @@ -249,9 +250,10 @@ class LoopManager: ObservableObject { Notification.Name.updateUIDirection.post(userInfo: ["action": self.currentAction]) if let screenToResizeOn = self.screenToResizeOn, + let window = self.targetWindow, !Defaults[.previewVisibility] { WindowEngine.resize( - self.targetWindow!, + window, to: self.currentAction, on: screenToResizeOn ) @@ -264,11 +266,11 @@ class LoopManager: ObservableObject { func handleMiddleClick(cgEvent: CGEvent) -> Unmanaged? { if let event = NSEvent(cgEvent: cgEvent), event.buttonNumber == 2, Defaults[.middleClickTriggersLoop] { - if event.type == .otherMouseDragged && !isLoopActive { + if event.type == .otherMouseDragged, !isLoopActive { openLoop() } - if event.type == .otherMouseUp && isLoopActive { + if event.type == .otherMouseUp, isLoopActive { closeLoop() } } @@ -345,7 +347,7 @@ class LoopManager: ObservableObject { let flags = event.modifierFlags.convertToCGKeyCode() if flags.count != currentlyPressedModifiers.count { for key in flags where CGKeyCode.keyToImage.contains(where: { $0.key == key }) { - if !currentlyPressedModifiers.map({ $0.baseModifier }).contains(key) { + if !currentlyPressedModifiers.map(\.baseModifier).contains(key) { currentlyPressedModifiers.insert(key) } } @@ -365,10 +367,10 @@ class LoopManager: ObservableObject { guard targetWindow?.isAppExcluded != true else { return } initialMousePosition = NSEvent.mouseLocation - screenToResizeOn = NSScreen.main + screenToResizeOn = Defaults[.useScreenWithCursor] ? NSScreen.screenWithMouse : NSScreen.main if !Defaults[.disableCursorInteraction] { - mouseMovedEventMonitor!.start() + mouseMovedEventMonitor?.start() } if !Defaults[.hideUntilDirectionIsChosen] { @@ -391,16 +393,16 @@ class LoopManager: ObservableObject { closeWindows() keybindMonitor.stop() - mouseMovedEventMonitor!.stop() + mouseMovedEventMonitor?.stop() currentlyPressedModifiers = [] if targetWindow != nil, - screenToResizeOn != nil, - forceClose == false, - currentAction.direction != .noAction, - isLoopActive { - if let screenToResizeOn = screenToResizeOn, + screenToResizeOn != nil, + forceClose == false, + currentAction.direction != .noAction, + isLoopActive { + if let screenToResizeOn, Defaults[.previewVisibility] { LoopManager.canAdjustSize = false WindowEngine.resize( @@ -417,7 +419,7 @@ class LoopManager: ObservableObject { Defaults[.timesLooped] += 1 IconManager.checkIfUnlockedNewIcon() } else { - if targetWindow == nil && isLoopActive { + if targetWindow == nil, isLoopActive { NSSound.beep() } } @@ -429,7 +431,7 @@ class LoopManager: ObservableObject { } private func openWindows() { - if Defaults[.previewVisibility] && targetWindow != nil { + if Defaults[.previewVisibility], targetWindow != nil { previewController.open(screen: screenToResizeOn!, window: targetWindow) } diff --git a/Loop/Managers/PermissionsManager.swift b/Loop/Managers/PermissionsManager.swift index 15f894cd..bb87aafd 100644 --- a/Loop/Managers/PermissionsManager.swift +++ b/Loop/Managers/PermissionsManager.swift @@ -1,12 +1,12 @@ // -// AccessibilityAccessManager.swift +// PermissionsManager.swift // Loop // // Created by Kai Azim on 2023-04-08. // -import SwiftUI import Defaults +import SwiftUI class AccessibilityManager { static func getStatus() -> Bool { diff --git a/Loop/Managers/ScreenManager.swift b/Loop/Managers/ScreenManager.swift index 9855dd2f..eb530ae6 100644 --- a/Loop/Managers/ScreenManager.swift +++ b/Loop/Managers/ScreenManager.swift @@ -9,12 +9,12 @@ import SwiftUI class ScreenManager { static func screenContaining(_ window: Window) -> NSScreen? { - let screens = self.getScreensInOrder() - return self.screenContaining(window, in: screens) + let screens = getScreensInOrder() + return screenContaining(window, in: screens) } static func nextScreen(from screen: NSScreen, canRestartCycle: Bool = true) -> NSScreen? { - let screens = self.getScreensInOrder() + let screens = getScreensInOrder() if let nextScreen = screens.next(from: screen) { return nextScreen } @@ -22,7 +22,7 @@ class ScreenManager { } static func previousScreen(from screen: NSScreen, canRestartCycle: Bool = true) -> NSScreen? { - let screens = self.getScreensInOrder() + let screens = getScreensInOrder() if let previousScreen = screens.previous(from: screen) { return previousScreen } @@ -40,7 +40,7 @@ class ScreenManager { return firstScreen } - guard let currentScreen = self.findScreen(with: window, screens) else { + guard let currentScreen = findScreen(with: window, screens) else { return firstScreen } @@ -86,11 +86,11 @@ class ScreenManager { extension Array where Element: Hashable { func next(from item: Element) -> Element? { - guard let index = self.firstIndex(of: item) else { + guard let index = firstIndex(of: item) else { return nil } - if index + 1 < self.count { + if index + 1 < count { return self[index + 1] } @@ -98,7 +98,7 @@ extension Array where Element: Hashable { } func previous(from item: Element) -> Element? { - guard let index = self.firstIndex(of: item) else { + guard let index = firstIndex(of: item) else { return nil } diff --git a/Loop/Managers/StageManager.swift b/Loop/Managers/StageManager.swift index f7432c38..f34e915f 100644 --- a/Loop/Managers/StageManager.swift +++ b/Loop/Managers/StageManager.swift @@ -20,6 +20,6 @@ class StageManager { } static var position: Edge { - dockDefaults?.string(forKey: "orientation") == "left" ? .trailing: .leading + dockDefaults?.string(forKey: "orientation") == "left" ? .trailing : .leading } } diff --git a/Loop/Managers/WindowDragManager.swift b/Loop/Managers/WindowDragManager.swift index 1fce60bc..028cd8a3 100644 --- a/Loop/Managers/WindowDragManager.swift +++ b/Loop/Managers/WindowDragManager.swift @@ -19,7 +19,7 @@ class WindowDragManager { private var leftMouseUpMonitor: EventMonitor? func addObservers() { - self.leftMouseDraggedMonitor = CGEventMonitor(eventMask: .leftMouseDragged) { cgEvent in + leftMouseDraggedMonitor = CGEventMonitor(eventMask: .leftMouseDragged) { cgEvent in // Process window (only ONCE during a window drag) if self.draggingWindow == nil { self.setCurrentDraggingWindow() @@ -28,7 +28,6 @@ class WindowDragManager { if let window = self.draggingWindow, let initialFrame = self.initialWindowFrame, self.hasWindowMoved(window.frame, initialFrame) { - if Defaults[.restoreWindowFrameOnDrag] { self.restoreInitialWindowSize(window) } else { @@ -49,11 +48,10 @@ class WindowDragManager { return Unmanaged.passUnretained(cgEvent) } - self.leftMouseUpMonitor = NSEventMonitor(scope: .global, eventMask: .leftMouseUp) { _ in + leftMouseUpMonitor = NSEventMonitor(scope: .global, eventMask: .leftMouseUp) { _ in if let window = self.draggingWindow, let initialFrame = self.initialWindowFrame, self.hasWindowMoved(window.frame, initialFrame) { - if Defaults[.windowSnapping] { self.attemptWindowSnap(window) } @@ -82,14 +80,14 @@ class WindowDragManager { } self.draggingWindow = draggingWindow - self.initialWindowFrame = draggingWindow.frame + initialWindowFrame = draggingWindow.frame } private func hasWindowMoved(_ windowFrame: CGRect, _ initialFrame: CGRect) -> Bool { !initialFrame.topLeftPoint.approximatelyEqual(to: windowFrame.topLeftPoint) && - !initialFrame.topRightPoint.approximatelyEqual(to: windowFrame.topRightPoint) && - !initialFrame.bottomLeftPoint.approximatelyEqual(to: windowFrame.bottomLeftPoint) && - !initialFrame.bottomRightPoint.approximatelyEqual(to: windowFrame.bottomRightPoint) + !initialFrame.topRightPoint.approximatelyEqual(to: windowFrame.topRightPoint) && + !initialFrame.bottomLeftPoint.approximatelyEqual(to: windowFrame.bottomLeftPoint) && + !initialFrame.bottomRightPoint.approximatelyEqual(to: windowFrame.bottomRightPoint) } private func restoreInitialWindowSize(_ window: Window) { @@ -132,38 +130,44 @@ class WindowDragManager { let mousePosition = NSEvent.mouseLocation.flipY(maxY: screen.frame.maxY) let screenFrame = screen.frame.flipY(maxY: screen.frame.maxY) - self.previewController.setScreen(to: screen) - let ignoredFrame = screenFrame.insetBy(dx: 2, dy: 2) + previewController.setScreen(to: screen) + + let insets: CGFloat = 2 + let topInset = screen.menubarHeight / 2 + var ignoredFrame = screenFrame + + ignoredFrame.origin.x += insets + ignoredFrame.size.width -= insets * 2 + ignoredFrame.origin.y += topInset + ignoredFrame.size.height -= insets + topInset - let oldDirection = self.direction + let oldDirection = direction if !ignoredFrame.contains(mousePosition) { - self.direction = WindowDirection.processSnap( + direction = WindowDirection.processSnap( mouseLocation: mousePosition, - currentDirection: self.direction, + currentDirection: direction, screenFrame: screenFrame, ignoredFrame: ignoredFrame ) print("Window snapping direction changed: \(direction)") - if Defaults[.previewVisibility] { - self.previewController.open(screen: screen, window: nil) + previewController.open(screen: screen, window: nil) - DispatchQueue.main.async { - NotificationCenter.default.post( - name: Notification.Name.updateUIDirection, - object: nil, - userInfo: ["action": WindowAction(self.direction)] - ) - } + DispatchQueue.main.async { + NotificationCenter.default.post( + name: Notification.Name.updateUIDirection, + object: nil, + userInfo: ["action": WindowAction(self.direction)] + ) } } else { - self.direction = .noAction - self.previewController.close() + direction = .noAction + previewController.close() } - if self.direction != oldDirection { + if direction != oldDirection { if Defaults[.hapticFeedback] { NSHapticFeedbackManager.defaultPerformer.perform( NSHapticFeedbackManager.FeedbackPattern.alignment, diff --git a/Loop/MenuBar/MenuBarHeaderText.swift b/Loop/MenuBar/MenuBarHeaderText.swift index 37b0f4dc..51088c5a 100644 --- a/Loop/MenuBar/MenuBarHeaderText.swift +++ b/Loop/MenuBar/MenuBarHeaderText.swift @@ -8,7 +8,8 @@ import SwiftUI struct MenuBarHeaderText: View { - public var label: String + var label: String + init(_ label: String) { self.label = label } diff --git a/Loop/MenuBar/MenuBarIconView.swift b/Loop/MenuBar/MenuBarIconView.swift index 1503a476..c8a25064 100644 --- a/Loop/MenuBar/MenuBarIconView.swift +++ b/Loop/MenuBar/MenuBarIconView.swift @@ -14,11 +14,11 @@ struct MenuBarIconView: View { // problem with only Loop's symbol symbol, but the circle.circle SF symbol also is slightly // off center. Will need to investigate that later. Image(.menubarIcon) - .rotationEffect(Angle.degrees(self.rotationAngle)) + .rotationEffect(Angle.degrees(rotationAngle)) .onReceive(.didLoop) { _ in - self.rotationAngle = 0 + rotationAngle = 0 withAnimation(.interpolatingSpring(stiffness: 100, damping: 15)) { - self.rotationAngle += 360 + rotationAngle += 360 } } } diff --git a/Loop/MenuBar/MenuBarResizeButton.swift b/Loop/MenuBar/MenuBarResizeButton.swift index 8fba0687..83450ec2 100644 --- a/Loop/MenuBar/MenuBarResizeButton.swift +++ b/Loop/MenuBar/MenuBarResizeButton.swift @@ -22,11 +22,11 @@ struct MenuBarResizeButton: View { } } label: { HStack { - if let image = direction.icon { - image - } else { - Image(systemName: "exclamationmark.triangle") - } +// if let image = direction.icon { +// image +// } else { +// Image(systemName: "exclamationmark.triangle") +// } Text(direction.name) } } diff --git a/Loop/Preview Window/LuminarePreviewView.swift b/Loop/Preview Window/LuminarePreviewView.swift new file mode 100644 index 00000000..4640cff1 --- /dev/null +++ b/Loop/Preview Window/LuminarePreviewView.swift @@ -0,0 +1,106 @@ +// +// LuminarePreviewView.swift +// Loop +// +// Created by Kai Azim on 2024-05-28. +// + +import Defaults +import SwiftUI + +struct LuminarePreviewView: View { + @State var action: WindowAction = .init(.topHalf) + @State var actionRect: CGRect = .zero + @State private var scale: CGFloat = 1 + @State var timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() + + @Default(.previewPadding) var previewPadding + @Default(.padding) var padding + @Default(.previewCornerRadius) var previewCornerRadius + @Default(.previewBorderThickness) var previewBorderThickness + @Default(.animationConfiguration) var animationConfiguration + + @Default(.useSystemAccentColor) var useSystemAccentColor + @Default(.customAccentColor) var customAccentColor + @Default(.useGradient) var useGradient + @Default(.gradientColor) var gradientColor + + @State var primaryColor: Color = .getLoopAccent(tone: .normal) + @State var secondaryColor: Color = .getLoopAccent(tone: Defaults[.useGradient] ? .darker : .normal) + @State var isActive: Bool = true + + var body: some View { + GeometryReader { geo in + ZStack { + VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) + .mask { + RoundedRectangle(cornerRadius: previewCornerRadius) + .foregroundColor(.white) + } + + RoundedRectangle(cornerRadius: previewCornerRadius) + .strokeBorder(.quinary, lineWidth: 1) + + RoundedRectangle(cornerRadius: previewCornerRadius) + .stroke( + LinearGradient( + gradient: Gradient( + colors: [ + isActive ? primaryColor : .systemGray, + isActive ? secondaryColor : .systemGray + ] + ), + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: previewBorderThickness + ) + } + .padding(previewPadding + previewBorderThickness / 2) + .frame(width: actionRect.width, height: actionRect.height) + .offset(x: actionRect.minX, y: actionRect.minY) + .scaleEffect(CGSize(width: scale, height: scale)) + .onAppear { + actionRect = action.getFrame(window: nil, bounds: .init(origin: .zero, size: geo.size)) + + withAnimation( + .interpolatingSpring( + duration: 0.2, + bounce: 0.1, + initialVelocity: 1 / 2 + ) + ) { + scale = 1 + } + } + .onReceive(timer) { _ in + guard isActive else { return } + + action.direction = action.direction.nextPreviewDirection + + withAnimation(animationConfiguration.previewTimingFunctionSwiftUI) { + actionRect = action.getFrame(window: nil, bounds: .init(origin: .zero, size: geo.size)) + } + } + } + .onChange(of: [customAccentColor, gradientColor]) { _ in + recomputeColors() + } + .onChange(of: [useSystemAccentColor, useGradient]) { _ in + recomputeColors() + } + .onReceive(.activeStateChanged) { notif in + if let active = notif.object as? Bool { + isActive = active + } + } + .clipShape(UnevenRoundedRectangle(bottomTrailingRadius: 10, topTrailingRadius: 10)) + } + + func recomputeColors() { + withAnimation(.smooth(duration: 0.25)) { + primaryColor = Color.getLoopAccent(tone: .normal) + secondaryColor = Color.getLoopAccent(tone: useGradient ? .darker : .normal) + } + } +} diff --git a/Loop/Preview Window/PreviewController.swift b/Loop/Preview Window/PreviewController.swift index 7254980b..49bc1d9d 100644 --- a/Loop/Preview Window/PreviewController.swift +++ b/Loop/Preview Window/PreviewController.swift @@ -5,11 +5,11 @@ // Created by Kai Azim on 2023-01-24. // -import SwiftUI import Defaults +import SwiftUI class PreviewController { - private var previewWindowController: NSWindowController? + var controller: NSWindowController? private var screen: NSScreen? private var window: Window? @@ -22,7 +22,7 @@ class PreviewController { } func open(screen: NSScreen, window: Window? = nil, startingAction: WindowAction? = nil) { - if let windowController = previewWindowController { + if let windowController = controller { windowController.window?.orderFrontRegardless() return } @@ -35,7 +35,7 @@ class PreviewController { screen: NSApp.keyWindow?.screen ) panel.alphaValue = 0 - panel.backgroundColor = NSColor.white.withAlphaComponent(0.00001) + panel.backgroundColor = .clear panel.setFrame(NSRect(origin: screen.stageStripFreeFrame.center, size: .zero), display: true) // This ensures that this is below the radial menu panel.level = NSWindow.Level(NSWindow.Level.screenSaver.rawValue - 1) @@ -43,19 +43,19 @@ class PreviewController { panel.collectionBehavior = .canJoinAllSpaces panel.ignoresMouseEvents = true panel.orderFrontRegardless() - previewWindowController = .init(window: panel) + controller = .init(window: panel) self.screen = screen self.window = window if let action = startingAction { - self.setAction(to: action) + setAction(to: action) } } func close() { - guard let windowController = previewWindowController else { return } - previewWindowController = nil + guard let windowController = controller else { return } + controller = nil windowController.window?.animator().alphaValue = 1 NSAnimationContext.runAnimationGroup({ _ in @@ -67,22 +67,22 @@ class PreviewController { func setScreen(to newScreen: NSScreen) { guard - self.previewWindowController != nil, // Ensures that the preview window is open - self.screen != newScreen + controller != nil, // Ensures that the preview window is open + screen != newScreen else { return } - self.close() - self.open(screen: newScreen, window: self.window) + close() + open(screen: newScreen, window: window) print("Changed preview window's screen") } func setAction(to action: WindowAction) { guard - let windowController = previewWindowController, - let screen = self.screen, + let windowController = controller, + let screen, !action.direction.willChangeScreen, action.direction != .cycle else { @@ -90,10 +90,10 @@ class PreviewController { } let targetWindowFrame = action.getFrame( - window: self.window, - bounds: screen.safeScreenFrame - ) - .flipY(maxY: NSScreen.screens[0].frame.maxY) + window: window, + bounds: screen.safeScreenFrame + ) + .flipY(maxY: NSScreen.screens[0].frame.maxY) let shouldBeTransparent = targetWindowFrame.size.area == 0 diff --git a/Loop/Preview Window/PreviewView.swift b/Loop/Preview Window/PreviewView.swift index 1763fe70..ace929f3 100644 --- a/Loop/Preview Window/PreviewView.swift +++ b/Loop/Preview Window/PreviewView.swift @@ -5,47 +5,43 @@ // Created by Kai Azim on 2023-01-24. // -import SwiftUI import Defaults +import SwiftUI struct PreviewView: View { - let previewMode: Bool - @State private var scale: CGFloat = 1 - - init(previewMode: Bool = false) { - self.previewMode = previewMode - - if previewMode { - self._scale = State(initialValue: 0) - } - } - - @Default(.useGradient) var useGradient @Default(.previewPadding) var previewPadding @Default(.padding) var padding @Default(.previewCornerRadius) var previewCornerRadius @Default(.previewBorderThickness) var previewBorderThickness @Default(.animationConfiguration) var animationConfiguration + @Default(.useSystemAccentColor) var useSystemAccentColor + @Default(.customAccentColor) var customAccentColor + @Default(.useGradient) var useGradient + @Default(.gradientColor) var gradientColor + + @State var primaryColor: Color = .getLoopAccent(tone: .normal) + @State var secondaryColor: Color = .getLoopAccent(tone: Defaults[.useGradient] ? .darker : .normal) + var body: some View { GeometryReader { _ in ZStack { VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) .mask { - RoundedRectangle(cornerRadius: previewCornerRadius, style: .continuous) + RoundedRectangle(cornerRadius: previewCornerRadius) .foregroundColor(.white) } - RoundedRectangle(cornerRadius: previewCornerRadius, style: .continuous) + RoundedRectangle(cornerRadius: previewCornerRadius) .strokeBorder(.quinary, lineWidth: 1) - RoundedRectangle(cornerRadius: previewCornerRadius, style: .continuous) + RoundedRectangle(cornerRadius: previewCornerRadius) .stroke( LinearGradient( gradient: Gradient( colors: [ - Color.getLoopAccent(tone: .normal), - Color.getLoopAccent(tone: useGradient ? .darker : .normal) + primaryColor, + secondaryColor ] ), startPoint: .topLeading, @@ -55,21 +51,6 @@ struct PreviewView: View { ) } .padding(previewPadding + previewBorderThickness / 2) - - .scaleEffect(CGSize(width: scale, height: scale)) - .onAppear { - if previewMode { - withAnimation( - .interpolatingSpring( - duration: 0.2, - bounce: 0.1, - initialVelocity: 1/2 - ) - ) { - self.scale = 1 - } - } - } } } } diff --git a/Loop/Radial Menu/DirectionSelectorCircleSegment.swift b/Loop/Radial Menu/DirectionSelectorCircleSegment.swift index 59ceb406..ee822efd 100644 --- a/Loop/Radial Menu/DirectionSelectorCircleSegment.swift +++ b/Loop/Radial Menu/DirectionSelectorCircleSegment.swift @@ -12,23 +12,23 @@ struct DirectionSelectorCircleSegment: Shape { let radialMenuSize: CGFloat var animatableData: Double { - get { self.angle } - set { self.angle = newValue } + get { angle } + set { angle = newValue } } - func path(in rect: CGRect) -> Path { + func path(in _: CGRect) -> Path { var path = Path() path.move( to: CGPoint( - x: radialMenuSize/2, - y: radialMenuSize/2 + x: radialMenuSize / 2, + y: radialMenuSize / 2 ) ) path.addArc( center: CGPoint( - x: radialMenuSize/2, - y: radialMenuSize/2 + x: radialMenuSize / 2, + y: radialMenuSize / 2 ), radius: radialMenuSize, startAngle: .degrees(angle - 22.5), diff --git a/Loop/Radial Menu/DirectionSelectorSquareSegment.swift b/Loop/Radial Menu/DirectionSelectorSquareSegment.swift index d763238c..ccdd3ae8 100644 --- a/Loop/Radial Menu/DirectionSelectorSquareSegment.swift +++ b/Loop/Radial Menu/DirectionSelectorSquareSegment.swift @@ -14,17 +14,17 @@ struct DirectionSelectorSquareSegment: View { var body: some View { ZStack { - RoundedRectangle(cornerRadius: radialMenuCornerRadius, style: .continuous) + RoundedRectangle(cornerRadius: radialMenuCornerRadius) .trim( - from: (Angle(degrees: self.angle - 22.5).normalized().degrees / 360.0), - to: (Angle(degrees: self.angle + 22.5).normalized().degrees / 360.0) + from: Angle(degrees: angle - 22.5).normalized().degrees / 360.0, + to: Angle(degrees: angle + 22.5).normalized().degrees / 360.0 ) .stroke(.white, lineWidth: radialMenuThickness * 2) - RoundedRectangle(cornerRadius: radialMenuCornerRadius, style: .continuous) + RoundedRectangle(cornerRadius: radialMenuCornerRadius) .trim( - from: (Angle(degrees: self.angle - 180 - 22.5).normalized().degrees / 360.0), - to: (Angle(degrees: self.angle - 180 + 22.5).normalized().degrees / 360.0) + from: Angle(degrees: angle - 180 - 22.5).normalized().degrees / 360.0, + to: Angle(degrees: angle - 180 + 22.5).normalized().degrees / 360.0 ) .stroke(.white, lineWidth: radialMenuThickness * 2) .rotationEffect(.degrees(180)) diff --git a/Loop/Radial Menu/RadialMenuController.swift b/Loop/Radial Menu/RadialMenuController.swift index e0e5bcc3..37a9358d 100644 --- a/Loop/Radial Menu/RadialMenuController.swift +++ b/Loop/Radial Menu/RadialMenuController.swift @@ -5,22 +5,21 @@ // Created by Kai Azim on 2023-01-23. // -import SwiftUI import Defaults +import SwiftUI class RadialMenuController { - private var loopRadialMenuWindowController: NSWindowController? + private var controller: NSWindowController? func open(position: CGPoint, frontmostWindow: Window?, startingAction: WindowAction = .init(.noAction)) { - if let windowController = loopRadialMenuWindowController { + if let windowController = controller { windowController.window?.orderFrontRegardless() return } let mouseX: CGFloat = position.x let mouseY: CGFloat = position.y - - let windowSize: CGFloat = 250 + let windowSize: CGFloat = 100 + 40 let panel = NSPanel( contentRect: .zero, @@ -32,7 +31,7 @@ class RadialMenuController { panel.collectionBehavior = .canJoinAllSpaces panel.hasShadow = false - panel.backgroundColor = NSColor.white.withAlphaComponent(0.00001) + panel.backgroundColor = .clear panel.level = .screenSaver panel.contentView = NSHostingView( rootView: RadialMenuView( @@ -41,18 +40,15 @@ class RadialMenuController { ) ) panel.alphaValue = 0 - panel.setFrame( - CGRect( - x: mouseX-windowSize/2, - y: mouseY-windowSize/2, - width: windowSize, - height: windowSize - ), - display: false + panel.setFrameOrigin( + NSPoint( + x: mouseX - windowSize / 2, + y: mouseY - windowSize / 2 + ) ) panel.orderFrontRegardless() - loopRadialMenuWindowController = .init(window: panel) + controller = .init(window: panel) NSAnimationContext.runAnimationGroup { context in context.duration = 0.15 @@ -61,8 +57,8 @@ class RadialMenuController { } func close() { - guard let windowController = loopRadialMenuWindowController else { return } - loopRadialMenuWindowController = nil + guard let windowController = controller else { return } + controller = nil windowController.window?.animator().alphaValue = 1 NSAnimationContext.runAnimationGroup({ context in diff --git a/Loop/Radial Menu/RadialMenuView.swift b/Loop/Radial Menu/RadialMenuView.swift index f64d9a8a..5a027a42 100644 --- a/Loop/Radial Menu/RadialMenuView.swift +++ b/Loop/Radial Menu/RadialMenuView.swift @@ -5,9 +5,9 @@ // Created by Kai Azim on 2023-01-24. // -import SwiftUI import Combine import Defaults +import SwiftUI struct RadialMenuView: View { let radialMenuSize: CGFloat = 100 @@ -23,163 +23,182 @@ struct RadialMenuView: View { // Variables that store the radial menu's shape @Default(.radialMenuCornerRadius) var radialMenuCornerRadius @Default(.radialMenuThickness) var radialMenuThickness - @Default(.useGradient) var useGradient @Default(.animationConfiguration) var animationConfiguration - init(previewMode: Bool = false, window: Window?, startingAction: WindowAction = .init(.noAction)) { + @Default(.useSystemAccentColor) var useSystemAccentColor + @Default(.customAccentColor) var customAccentColor + @Default(.useGradient) var useGradient + @Default(.gradientColor) var gradientColor + + init(previewMode: Bool = false, window: Window? = nil, startingAction: WindowAction = .init(.noAction)) { self.window = window self.previewMode = previewMode - self._currentAction = State(initialValue: .init(startingAction.direction)) if previewMode { self._timer = State(initialValue: Timer.publish(every: 1, on: .main, in: .common).autoconnect()) + self._currentAction = State(initialValue: .init(.topHalf)) } else { self._timer = State(initialValue: Timer.publish(every: -1, on: .main, in: .common).autoconnect()) + self._currentAction = State(initialValue: .init(startingAction.direction)) } } @State var angle: Double = .zero + @State var primaryColor: Color = .getLoopAccent(tone: .normal) + @State var secondaryColor: Color = .getLoopAccent(tone: Defaults[.useGradient] ? .darker : .normal) + @State var isActive: Bool = true + var body: some View { - VStack { - Spacer() - HStack { - Spacer() - - ZStack { - ZStack { - // NSVisualEffect on background - VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) - - // This rectangle with a gradient is masked with the current direction radial menu view - Rectangle() - .fill( - LinearGradient( - gradient: Gradient( - colors: [ - Color.getLoopAccent(tone: .normal), - Color.getLoopAccent(tone: useGradient ? .darker : .normal) - ] - ), - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - ) - .mask { - Color.clear - .overlay { - ZStack { - if self.currentAction.direction.shouldFillRadialMenu { - Color.white - } - - ZStack { - if radialMenuCornerRadius >= radialMenuSize / 2 - 2 { - DirectionSelectorCircleSegment( - angle: self.angle, - radialMenuSize: self.radialMenuSize - ) - } else { - DirectionSelectorSquareSegment( - angle: self.angle, - radialMenuCornerRadius: self.radialMenuCornerRadius, - radialMenuThickness: self.radialMenuThickness - ) - } - } - .compositingGroup() - .opacity( - !self.currentAction.direction.hasRadialMenuAngle || - self.currentAction.direction == .custom ? - 0 : 1 + ZStack { + ZStack { + // NSVisualEffect on background + VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) + + // This rectangle with a gradient is masked with the current direction radial menu view + Rectangle() + .fill( + LinearGradient( + gradient: Gradient( + colors: [ + !previewMode || isActive ? primaryColor : .systemGray, + !previewMode || isActive ? secondaryColor : .systemGray + ] + ), + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .mask { + Color.clear + .overlay { + ZStack { + if currentAction.direction.shouldFillRadialMenu { + Color.white + } + + ZStack { + if radialMenuCornerRadius >= radialMenuSize / 2 - 2 { + DirectionSelectorCircleSegment( + angle: angle, + radialMenuSize: radialMenuSize + ) + } else { + DirectionSelectorSquareSegment( + angle: angle, + radialMenuCornerRadius: radialMenuCornerRadius, + radialMenuThickness: radialMenuThickness ) } } + .compositingGroup() + .opacity( + !currentAction.direction.hasRadialMenuAngle || + currentAction.direction == .custom ? + 0 : 1 + ) + } } - - if radialMenuCornerRadius >= radialMenuSize / 2 - 2 { - Circle() - .stroke(.quinary, lineWidth: 2) - - Circle() - .stroke(.quinary, lineWidth: 2) - .padding(self.radialMenuThickness) - } else { - RoundedRectangle(cornerRadius: radialMenuCornerRadius, style: .continuous) - .stroke(.quinary, lineWidth: 2) - - RoundedRectangle( - cornerRadius: radialMenuCornerRadius - self.radialMenuThickness, - style: .continuous - ) - .stroke(.quinary, lineWidth: 2) - .padding(self.radialMenuThickness) - } - } - // Mask the whole ZStack with the shape the user defines - .mask { - if radialMenuCornerRadius >= radialMenuSize / 2 - 2 { - Circle() - .strokeBorder(.black, lineWidth: radialMenuThickness) - } else { - RoundedRectangle(cornerRadius: radialMenuCornerRadius, style: .continuous) - .strokeBorder(.black, lineWidth: radialMenuThickness) - } } - Group { - if window == nil && previewMode == false { - Image("custom.macwindow.trianglebadge.exclamationmark") - } else if let image = self.currentAction.direction.radialMenuImage { - image - } - } - .foregroundStyle(Color.getLoopAccent(tone: .normal)) - .font(Font.system(size: 20, weight: .bold)) + if radialMenuCornerRadius >= radialMenuSize / 2 - 2 { + Circle() + .stroke(.quinary, lineWidth: 2) + + Circle() + .stroke(.quinary, lineWidth: 2) + .padding(radialMenuThickness) + } else { + RoundedRectangle(cornerRadius: radialMenuCornerRadius) + .stroke(.quinary, lineWidth: 2) + + RoundedRectangle(cornerRadius: radialMenuCornerRadius - radialMenuThickness) + .stroke(.quinary, lineWidth: 2) + .padding(radialMenuThickness) + } + } + // Mask the whole ZStack with the shape the user defines + .mask { + if radialMenuCornerRadius >= radialMenuSize / 2 - 2 { + Circle() + .strokeBorder(.black, lineWidth: radialMenuThickness) + } else { + RoundedRectangle(cornerRadius: radialMenuCornerRadius) + .strokeBorder(.black, lineWidth: radialMenuThickness) } - .frame(width: radialMenuSize, height: radialMenuSize) + } - Spacer() + Group { + if window == nil, previewMode == false { + Image("custom.macwindow.trianglebadge.exclamationmark") + } else if let image = currentAction.radialMenuImage { + image + } } - Spacer() + .foregroundStyle(Color.getLoopAccent(tone: .normal)) + .font(Font.system(size: 20, weight: .bold)) } + .frame(width: radialMenuSize, height: radialMenuSize) .shadow(radius: 10) + .padding(20) + .fixedSize() // Animate window .scaleEffect(currentAction.direction == .maximize ? 0.85 : 1) .animation(animationConfiguration.radialMenuSize, value: currentAction) .onAppear { - if previewMode { - currentAction.direction = currentAction.direction.nextPreviewDirection - } + recomputeAngle() } .onReceive(timer) { _ in if previewMode { + guard isActive else { return } previousAction = currentAction currentAction.direction = currentAction.direction.nextPreviewDirection } } .onReceive(.updateUIDirection) { obj in - if !self.previewMode, let action = obj.userInfo?["action"] as? WindowAction { - self.previousAction = self.currentAction - self.currentAction = .init(action.direction) + if !previewMode, let action = obj.userInfo?["action"] as? WindowAction { + previousAction = currentAction + currentAction = .init(action.direction) print("New radial menu window action received: \(action.direction)") } } - .onChange(of: self.currentAction) { _ in - if let target = self.currentAction.radialMenuAngle(window: window) { - let closestAngle: Angle = .degrees(self.angle).angleDifference(to: target) + .onChange(of: currentAction) { _ in + recomputeAngle() + } + .onChange(of: [customAccentColor, gradientColor]) { _ in + recomputeColors() + } + .onChange(of: [useSystemAccentColor, useGradient]) { _ in + recomputeColors() + } + .onReceive(.activeStateChanged) { notif in + if let active = notif.object as? Bool { + isActive = active + } + } + } + + func recomputeColors() { + withAnimation(.smooth(duration: 0.25)) { + primaryColor = Color.getLoopAccent(tone: .normal) + secondaryColor = Color.getLoopAccent(tone: useGradient ? .darker : .normal) + } + } - let previousActionHadAngle = self.previousAction?.direction.hasRadialMenuAngle ?? false - let animate: Bool = abs(closestAngle.degrees) < 179 && previousActionHadAngle + func recomputeAngle() { + if let target = currentAction.radialMenuAngle(window: window) { + let closestAngle: Angle = .degrees(angle).angleDifference(to: target) - let defaultAnimation = AnimationConfiguration.fast.radialMenuAngle - let noAnimation = Animation.linear(duration: 0) + let previousActionHadAngle = previousAction?.direction.hasRadialMenuAngle ?? false + let animate: Bool = abs(closestAngle.degrees) < 179 && previousActionHadAngle - withAnimation(animate ? defaultAnimation : noAnimation) { - self.angle += closestAngle.degrees - } + let defaultAnimation = AnimationConfiguration.fast.radialMenuAngle + let noAnimation = Animation.linear(duration: 0) + + withAnimation(animate ? defaultAnimation : noAnimation) { + angle += closestAngle.degrees } } } diff --git a/Loop/Settings/ExcludeListSettingsView.swift b/Loop/Settings/ExcludeListSettingsView.swift deleted file mode 100644 index 8241c0b6..00000000 --- a/Loop/Settings/ExcludeListSettingsView.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// ExcludeListSettingsView.swift -// Loop -// -// Created by Dirk Mika on 11.03.24. -// - -import SwiftUI -import Defaults - -struct ExcludeListSettingsView: View { - @EnvironmentObject var appListManager: AppListManager - - @Default(.applicationExcludeList) var excludeList - @State private var selection = Set() - - var body: some View { - ZStack { - Form { - Section { - VStack(spacing: 0) { - if self.excludeList.isEmpty { - HStack { - Spacer() - VStack { - Text("No Excluded Applications") - .font(.title3) - Text("Press + to add an application!") - .font(.caption) - } - Spacer() - } - .foregroundStyle(.secondary) - .padding() - } else { - List(selection: $selection) { - ForEach(self.$excludeList, id: \.self) { entry in - Group { - if let app = appListManager.installedApps.first(where: { - $0.bundleID == entry.wrappedValue - }) { - HStack { - Image(nsImage: app.icon) - Text(app.displayName) - .padding(.leading, 2) - } - } else { - Text(entry.wrappedValue) - } - } - .padding(.vertical, 5) - .contextMenu { - Button("Delete") { - if self.selection.isEmpty { - self.excludeList.removeAll { - $0 == entry.wrappedValue - } - } else { - for item in selection { - self.excludeList.removeAll { - $0 == item - } - } - self.selection.removeAll() - } - } - } - .tag(entry.wrappedValue) - } - .onMove { indices, newOffset in - self.excludeList.move(fromOffsets: indices, toOffset: newOffset) - } - .onDelete { offset in - self.excludeList.remove(atOffsets: offset) - } - } - .listStyle(.bordered(alternatesRowBackgrounds: true)) - } - - Divider() - - Rectangle() - .frame(height: 20) - .foregroundStyle(.quinary) - .overlay { - HStack(spacing: 5) { - Menu(content: { - installedAppsMenu() - }, label: { - Rectangle() - .foregroundStyle(.white.opacity(0.00001)) - .overlay { - Image(systemName: "plus") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - } - .aspectRatio(1, contentMode: .fit) - .padding(-5) - }) - - Divider() - - Button { - for item in selection { - self.excludeList.removeAll { - $0 == item - } - } - self.selection.removeAll() - } label: { - Rectangle() - .foregroundStyle(.white.opacity(0.00001)) - .overlay { - Image(systemName: "minus") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - } - .aspectRatio(1, contentMode: .fit) - .padding(-5) - } - .disabled(self.selection.isEmpty) - - Spacer() - } - .buttonStyle(.plain) - .padding(5) - } - } - .ignoresSafeArea() - .padding(-10) - } header: { - VStack(alignment: .leading) { - Text("Excluded Applications") - Text("Applications in the exclude list are ignored by \(Bundle.main.appName).") - .font(.caption) - .foregroundColor(.secondary) - } - } - } - .formStyle(.grouped) - } - } - - @ViewBuilder - func installedAppsMenu() -> some View { - let apps = appListManager.installedApps - .filter { - !self.excludeList.contains($0.bundleID) - } - .grouped { - $0.installationFolder - } - let installationFolders = apps.keys.sorted { - $0.localizedCaseInsensitiveCompare($1) == .orderedAscending - } - - ForEach(installationFolders, id: \.self) { folder in - Section(folder) { - let appsInFolder = apps[folder]!.sorted { - $0.displayName.localizedCaseInsensitiveCompare($1.displayName) == .orderedAscending - } - ForEach(appsInFolder) { app in - Button(action: { - self.excludeList.append(app.bundleID) - }, label: { - // Resizing the image with SwiftUI did not work. Therefore we change the size of the NSImage. - Image(nsImage: app.icon.resized(to: NSSize(width: 16.0, height: 16.0))) - Text(app.displayName) - }) - } - } - } - } -} diff --git a/Loop/Settings/GeneralSettingsView.swift b/Loop/Settings/GeneralSettingsView.swift index 5d10fb08..85c7f28d 100644 --- a/Loop/Settings/GeneralSettingsView.swift +++ b/Loop/Settings/GeneralSettingsView.swift @@ -5,9 +5,9 @@ // Created by Kai Azim on 2023-01-24. // -import SwiftUI import Defaults import ServiceManagement +import SwiftUI struct GeneralSettingsView: View { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @@ -29,174 +29,17 @@ struct GeneralSettingsView: View { @Default(.focusWindowOnResize) var focusWindowOnResize @State var userDisabledLoopNotifications: Bool = false - @State var iconFooter: String? @State var isConfiguringPadding: Bool = false var body: some View { Form { - Section("Behavior") { - Toggle("Launch at login", isOn: $launchAtLogin) - .onChange(of: launchAtLogin) { _ in - if launchAtLogin { - try? SMAppService().register() - } else { - try? SMAppService().unregister() - } - } - - VStack(alignment: .leading) { - Toggle("Hide menubar icon", isOn: $hideMenuBarIcon) - - if hideMenuBarIcon { - Text("Re-open \(Bundle.main.appName) to see this window.") - .font(.caption) - .foregroundColor(.secondary) - .textSelection(.enabled) - } - } - } - - Section { - Toggle("Window snapping", isOn: $windowSnapping) - - HStack { - Text("Window padding") - Spacer() - Button("Configure…") { - self.isConfiguringPadding = true - } - } - .sheet(isPresented: self.$isConfiguringPadding) { - PaddingConfigurationView(isSheetShown: $isConfiguringPadding, paddingModel: $padding) - } - - Toggle( - "Restore window frame on drag", - isOn: $restoreWindowFrameOnDrag - ) - - Toggle(isOn: $resizeWindowUnderCursor) { - VStack(alignment: .leading) { - Text("Resize window under cursor") - Text( - resizeWindowUnderCursor ? - "Resizes window under cursor, and uses the frontmost window as backup." : - "Resizes frontmost window." - ) - .font(.caption) - .foregroundStyle(.secondary) - } - } - - if resizeWindowUnderCursor { - Toggle("Focus window on resize", isOn: $focusWindowOnResize) - } - } - - Section { - Picker("Animation speed", selection: $animationConfiguration) { - ForEach(AnimationConfiguration.allCases) { configuration in - Text(configuration.name) - } - } - } - Section("App Icon") { VStack(alignment: .leading) { - Picker("Selected icon:", selection: $currentIcon) { - ForEach(IconManager.returnUnlockedIcons(), id: \.self) { icon in - HStack { - Image(nsImage: NSImage(named: icon.iconName)!) - Text(icon.getName()) - } - .tag(icon.iconName) - } - } - - VStack(alignment: .leading) { - Text("Loop more to unlock new icons! (You've looped \(timesLooped) times!)") - - if let iconFooter = iconFooter { - Text(iconFooter) - } - } - .font(.caption) - .foregroundColor(.secondary) - .textSelection(.enabled) - } - .onAppear { - self.iconFooter = IconManager.currentAppIcon.footer - } - .onChange(of: self.currentIcon) { _ in - IconManager.refreshCurrentAppIcon() - self.iconFooter = IconManager.currentAppIcon.footer - } - - Toggle( - "Notify when new icons are unlocked", - isOn: Binding( - get: { - self.notificationWhenIconUnlocked - }, - set: { - if $0 { - let notficationBody: String = .init( - localized: .init( - "Default notification content", - defaultValue: "You will now be notified when you unlock a new icon." - ) - ) - AppDelegate.sendNotification(Bundle.main.appName, notficationBody) - - let areNotificationsEnabled = AppDelegate.areNotificationsEnabled() - self.notificationWhenIconUnlocked = areNotificationsEnabled - - if !areNotificationsEnabled { - self.userDisabledLoopNotifications = true - } - } else { - self.notificationWhenIconUnlocked = $0 - } - } - ) - ) - .onAppear { - if self.notificationWhenIconUnlocked { - self.notificationWhenIconUnlocked = AppDelegate.areNotificationsEnabled() - } - } - .popover( - isPresented: self.$userDisabledLoopNotifications, - arrowEdge: .bottom - ) { - VStack(alignment: .center) { - Text("\(Bundle.main.appName)'s notification permissions are currently disabled.") - Text("Please turn them on in System Settings.") - Button("Open notification settings") { - NSWorkspace.shared.open( - URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension")! - ) - } - } - .padding(8) - } - } - - Section("Accent Color") { - Toggle("Use system accent color", isOn: $useSystemAccentColor) - - if !useSystemAccentColor { - ColorPicker("Custom accent color", selection: $customAccentColor, supportsOpacity: false) - } - - Toggle("Use gradient", isOn: $useGradient) - - if !useSystemAccentColor && useGradient { - ColorPicker("Custom gradient color", selection: $gradientColor, supportsOpacity: false) - .foregroundColor( - useGradient ? (useSystemAccentColor ? .secondary : nil) : .secondary - ) + Text("Loop more to unlock new icons! (You've looped \(timesLooped) times!)") + .font(.caption) + .foregroundColor(.secondary) + .textSelection(.enabled) } } } diff --git a/Loop/Settings/Keybindings/Custom Cycling Keybinds/CustomCyclingKeybindItemView.swift b/Loop/Settings/Keybindings/Custom Cycling Keybinds/CustomCyclingKeybindItemView.swift deleted file mode 100644 index 8e93a8ac..00000000 --- a/Loop/Settings/Keybindings/Custom Cycling Keybinds/CustomCyclingKeybindItemView.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// CustomCyclingKeybindItemView.swift -// Loop -// -// Created by Kai Azim on 2023-12-28. -// - -import SwiftUI - -struct CustomCyclingKeybindItemView: View { - @Binding var action: WindowAction - @Binding var total: [WindowAction] - - @State var isConfiguringCustomKeybind: Bool = false - - var body: some View { - HStack { - directionPicker(selection: $action.direction) - - if self.action.direction == .custom { - Button(action: { - self.isConfiguringCustomKeybind.toggle() - }, label: { - Image(systemName: "slider.horizontal.3") - .font(.title3) - .foregroundStyle(.secondary) - }) - .buttonStyle(.plain) - .sheet(isPresented: self.$isConfiguringCustomKeybind) { - CustomKeybindView(action: $action, isSheetShown: $isConfiguringCustomKeybind) - } - } - Spacer() - - Text("\(self.total.firstIndex(of: action) ?? -1)") - .foregroundStyle(.secondary) - .fontDesign(.monospaced) - } - .padding(.vertical, 5) - } - - @ViewBuilder - func directionPicker(selection: Binding) -> some View { - Menu(content: { - Picker("General", selection: $action.direction) { - ForEach(WindowDirection.general) { direction in - directionPickerItem(direction) - } - } - - Picker("Halves", selection: $action.direction) { - ForEach(WindowDirection.halves) { direction in - directionPickerItem(direction) - } - } - - Picker("Quarters", selection: $action.direction) { - ForEach(WindowDirection.quarters) { direction in - directionPickerItem(direction) - } - } - - Picker("Horizontal Thirds", selection: $action.direction) { - ForEach(WindowDirection.horizontalThirds) { direction in - directionPickerItem(direction) - } - } - - Picker("Vertical Thirds", selection: $action.direction) { - ForEach(WindowDirection.verticalThirds) { direction in - directionPickerItem(direction) - } - } - - Picker("More", selection: $action.direction) { - ForEach(WindowDirection.more) { direction in - if direction != .cycle { - directionPickerItem(direction) - } - } - } - }, label: { - HStack { - self.action.direction.icon - Text( - action.direction == .custom - ? action.name ?? .init(localized: .init("Custom keybind", defaultValue: "Custom keybind")) - : action.direction.name - ) - } - }) - .fixedSize() - } - - @ViewBuilder - func directionPickerItem(_ direction: WindowDirection) -> some View { - HStack { - direction.icon - Text(direction.name) - } - .tag(direction) - } -} diff --git a/Loop/Settings/Keybindings/Custom Cycling Keybinds/CustomCyclingKeybindView.swift b/Loop/Settings/Keybindings/Custom Cycling Keybinds/CustomCyclingKeybindView.swift deleted file mode 100644 index a06dc51b..00000000 --- a/Loop/Settings/Keybindings/Custom Cycling Keybinds/CustomCyclingKeybindView.swift +++ /dev/null @@ -1,206 +0,0 @@ -// -// CustomCyclingKeybindView.swift -// Loop -// -// Created by Kai Azim on 2023-12-27. -// - -import SwiftUI - -struct CustomCyclingKeybindView: View { - @Binding var action: WindowAction - @Binding var isSheetShown: Bool - - @FocusState private var focusedField: String? - - @State var cycleDirections: [WindowAction] = [] - @State private var selection: WindowAction? - - var body: some View { - VStack { - Form { - Section { - TextField("Name", text: $action.name.bound, prompt: Text("Custom cycle")) - .focused($focusedField, equals: "name") - } - - Section { - VStack(spacing: 0) { - if self.cycleDirections.isEmpty { - HStack { - Spacer() - VStack { - Text("Nothing to cycle through") - .font(.title3) - Text("Press + to add a cycle item!") - .font(.caption) - } - Spacer() - } - .foregroundStyle(.secondary) - .padding() - } else { - List(selection: $selection) { - ForEach(self.$cycleDirections) { cycleAction in - CustomCyclingKeybindItemView(action: cycleAction, total: self.$cycleDirections) - .contextMenu { - Button { - self.cycleDirections.removeAll { - $0 == cycleAction.wrappedValue - } - } label: { - Label("Delete", systemImage: "trash") - } - } - .tag(cycleAction.wrappedValue) - } - .onMove { indices, newOffset in - self.cycleDirections.move(fromOffsets: indices, toOffset: newOffset) - } - } - .listStyle(.bordered(alternatesRowBackgrounds: true)) - } - - Divider() - - Rectangle() - .frame(height: 20) - .foregroundStyle(.quinary) - .overlay { - HStack(spacing: 5) { - Menu(content: { - newDirectionMenu() - }, label: { - Rectangle() - .foregroundStyle(.white.opacity(0.00001)) - .overlay { - Image(systemName: "plus") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - } - .aspectRatio(1, contentMode: .fit) - .padding(-5) - }) - - Divider() - - Button { - self.cycleDirections.removeAll { - $0 == selection - } - } label: { - Rectangle() - .foregroundStyle(.white.opacity(0.00001)) - .overlay { - Image(systemName: "minus") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - } - .aspectRatio(1, contentMode: .fit) - .padding(-5) - } - .disabled(self.selection == nil) - - Spacer() - } - .buttonStyle(.plain) - .padding(5) - } - } - .ignoresSafeArea() - .padding(-10) - } - } - .onTapGesture { - focusedField = nil - } - .formStyle(.grouped) - .scrollDisabled(true) - - HStack { - Button { - isSheetShown = false - } label: { - Text("Done") - } - .controlSize(.large) - } - .offset(y: -14) - } - .frame(width: 450) - .fixedSize(horizontal: false, vertical: true) - .background(.background) - - .onAppear { - self.cycleDirections = self.action.cycle ?? [] - } - .onChange(of: self.cycleDirections) { _ in - if self.cycleDirections.isEmpty { - self.action.cycle = nil - } else { - self.action.cycle = self.cycleDirections - } - } - } - - @ViewBuilder - func newDirectionMenu() -> some View { - Menu("General") { - ForEach(WindowDirection.general) { direction in - newDirectionButton(direction) - } - } - - Menu("Halves") { - ForEach(WindowDirection.halves) { direction in - newDirectionButton(direction) - } - } - - Menu("Quarters") { - ForEach(WindowDirection.quarters) { direction in - newDirectionButton(direction) - } - } - - Menu("Horizontal Thirds") { - ForEach(WindowDirection.horizontalThirds) { direction in - newDirectionButton(direction) - } - } - - Menu("Vertical Thirds") { - ForEach(WindowDirection.verticalThirds) { direction in - newDirectionButton(direction) - } - } - - Menu("Screen Switching") { - ForEach(WindowDirection.screenSwitching) { direction in - newDirectionButton(direction) - } - } - - Menu("More") { - ForEach(WindowDirection.more) { direction in - if direction != .cycle { - newDirectionButton(direction) - } - } - } - } - - @ViewBuilder - func newDirectionButton(_ direction: WindowDirection) -> some View { - Button(action: { - self.cycleDirections.append(WindowAction(direction, keybind: [])) - }, label: { - HStack { - direction.icon - Text(direction.name) - } - }) - } -} diff --git a/Loop/Settings/Keybindings/Custom Keybinds/AnchorPicker.swift b/Loop/Settings/Keybindings/Custom Keybinds/AnchorPicker.swift deleted file mode 100644 index 30dc8116..00000000 --- a/Loop/Settings/Keybindings/Custom Keybinds/AnchorPicker.swift +++ /dev/null @@ -1,77 +0,0 @@ -// -// AnchorPicker.swift -// Loop -// -// Created by Kai Azim on 2023-12-24. -// - -import SwiftUI - -struct AnchorPicker: View { - @Namespace private var animation - @Binding var anchor: CustomWindowActionAnchor? - - var body: some View { - VStack { - HStack { - selectorCircle(.topLeft) - Spacer() - selectorCircle(.top) - Spacer() - selectorCircle(.topRight) - } - - Spacer() - - HStack { - selectorCircle(.left) - Spacer() - selectorCircle([.center, .macOSCenter]) - Spacer() - selectorCircle(.right) - } - - Spacer() - - HStack { - selectorCircle(.bottomLeft) - Spacer() - selectorCircle(.bottom) - Spacer() - selectorCircle(.bottomRight) - } - } - .animation(.snappy, value: self.anchor) - .padding(8) - } - - @ViewBuilder - func selectorCircle(_ anchor: CustomWindowActionAnchor) -> some View { - selectorCircle([anchor]) - } - - @ViewBuilder - func selectorCircle(_ anchor: [CustomWindowActionAnchor]) -> some View { - Button { - self.anchor = anchor.first - } label: { - Circle() - .foregroundStyle(Color.accentColor) - .frame(width: 16, height: 16) - .overlay { - Circle() - .strokeBorder(.white.opacity(0.5), lineWidth: 1) - } - .overlay { - if anchor.contains(where: { $0 == self.anchor }) { - Circle() - .foregroundStyle(.white) - .frame(width: 6, height: 6) - .matchedGeometryEffect(id: "selection", in: animation) - } - } - } - .buttonStyle(.plain) - .shadow(radius: 10) - } -} diff --git a/Loop/Settings/Keybindings/Custom Keybinds/CustomKeybindView.swift b/Loop/Settings/Keybindings/Custom Keybinds/CustomKeybindView.swift deleted file mode 100644 index 3a59b17f..00000000 --- a/Loop/Settings/Keybindings/Custom Keybinds/CustomKeybindView.swift +++ /dev/null @@ -1,273 +0,0 @@ -// -// CustomKeybindView.swift -// Loop -// -// Created by Kai Azim on 2023-12-24. -// - -import SwiftUI - -struct CustomKeybindView: View { - @Binding var action: WindowAction - @Binding var isSheetShown: Bool - @State var showingInfo: Bool = false - - @FocusState private var focusedField: String? - - var body: some View { - VStack { - Form { - Section { - TextField("Name", text: $action.name.bound, prompt: Text("Custom keybind")) - .focused($focusedField, equals: "name") - - Picker("Measurement unit", selection: $action.unit) { - ForEach(CustomWindowActionUnit.allCases) { system in - system.label - .tag(system as CustomWindowActionUnit?) - } - } - .onChange(of: action.unit) { _ in - if action.unit == .percentage { - if action.width ?? 101 > 100 { - self.action.width = 100 - } - if action.height ?? 101 > 100 { - self.action.height = 100 - } - - if action.xPoint ?? 101 > 100 { - self.action.xPoint = 100 - } - if action.yPoint ?? 101 > 100 { - self.action.xPoint = 100 - } - } - } - } - - Section { - Picker("Position mode", selection: $action.positionMode) { - ForEach(CustomWindowActionPositionMode.allCases) { system in - system.label - .tag(system as CustomWindowActionPositionMode?) - } - } - - if let positionMode = action.positionMode, positionMode == .coordinates { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: X", defaultValue: "X")), - value: Binding( - get: { - self.action.xPoint ?? 0 - }, - set: { - self.action.xPoint = $0 - - if let width = action.width, - let sizeMode = action.sizeMode, - sizeMode == .custom, - let unit = action.unit, - unit == .percentage { - action.width = min(width, 100 - $0) - } - } - ), - sliderRange: action.unit == .percentage ? 0...100 : 0...( - Double(NSScreen.main?.frame.width ?? 1000) - ), - postscript: action.unit?.postscript ?? "", - lowerClamp: true - ) - - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Y", defaultValue: "Y")), - value: Binding( - get: { - self.action.yPoint ?? 0 - }, - set: { - self.action.yPoint = $0 - - if let height = action.height, - let sizeMode = action.sizeMode, - sizeMode == .custom, - let unit = action.unit, - unit == .percentage { - action.height = min(height, 100 - $0) - } - } - ), - sliderRange: action.unit == .percentage ? 0...100 : 0...( - Double(NSScreen.main?.frame.height ?? 1000) - ), - postscript: action.unit?.postscript ?? "", - lowerClamp: true - ) - } - } header: { - Text("Window position") - } footer: { - if let positionMode = action.positionMode, positionMode == .coordinates { - Text("This point determines the upper-left edge of the window.") - .font(.caption) - .foregroundStyle(.secondary) - } - } - - if let positionMode = action.positionMode, positionMode == .generic { - Section { - ZStack { - WallpaperView().equatable() - AnchorPicker(anchor: self.$action.anchor) - } - .ignoresSafeArea() - .padding(-10) - .aspectRatio(16/10, contentMode: .fit) - } - - if self.action.anchor == .center || self.action.anchor == .macOSCenter { - Section { - Toggle( - isOn: Binding( - get: { self.action.anchor == .macOSCenter }, - set: { self.action.anchor = $0 ? .macOSCenter : .center } - ) - ) { - HStack { - Text("Use macOS Center") - if let moreInformation = WindowDirection.macOSCenter.moreInformation { - Button(action: { - self.showingInfo.toggle() - }, label: { - Image(systemName: "info.circle") - .font(.title3) - .foregroundStyle(.secondary) - }) - .buttonStyle(.plain) - .popover(isPresented: $showingInfo, arrowEdge: .bottom) { - Text(moreInformation) - .multilineTextAlignment(.center) - .padding(8) - } - } - } - } - } - } - } - - Section("Window size") { - Picker("Sizing mode", selection: $action.sizeMode) { - ForEach(CustomWindowActionSizeMode.allCases) { system in - system.label - .tag(system as CustomWindowActionSizeMode?) - } - } - - if let sizeMode = action.sizeMode, sizeMode == .custom { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Width", defaultValue: "Width")), - value: Binding( - get: { - self.action.width ?? 0 - }, - set: { - self.action.width = $0 - - if let xPoint = action.xPoint, - let positionMode = action.positionMode, - positionMode == .coordinates, - let unit = action.unit, - unit == .percentage { - action.xPoint = min(xPoint, 100 - $0) - } - } - ), - sliderRange: action.unit == .percentage ? 0...100 : 0...( - Double(NSScreen.main?.frame.width ?? 1000) - ), - postscript: action.unit?.postscript ?? "", - lowerClamp: true - ) - - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Height", defaultValue: "Height")), - value: Binding( - get: { - self.action.height ?? 0 - }, - set: { - self.action.height = $0 - - if let yPoint = action.yPoint, - let positionMode = action.positionMode, - positionMode == .coordinates, - let unit = action.unit, - unit == .percentage { - action.yPoint = min(yPoint, 100 - $0) - } - } - ), - sliderRange: action.unit == .percentage ? 0...100 : 0...( - Double(NSScreen.main?.frame.height ?? 1000) - ), - postscript: action.unit?.postscript ?? "", - lowerClamp: true - ) - } - } - - Section { - HStack { - Text("Preview window size") - Spacer() - PreviewWindowButton(self.$action) - .disabled({ - if let sizeMode = action.sizeMode { - return sizeMode != .custom - } - return false - }()) - } - } - } - .onTapGesture { - focusedField = nil - } - .formStyle(.grouped) - .scrollDisabled(true) - - HStack { - Button { - isSheetShown = false - } label: { - Text("Done") - } - .controlSize(.large) - } - .offset(y: -14) - } - .frame(width: 400) - .fixedSize(horizontal: false, vertical: true) - .background(.background) - - .onAppear { - if self.action.unit == nil { - self.action.unit = .percentage - } - - if self.action.sizeMode == nil { - self.action.sizeMode = .custom - } - - if self.action.positionMode == nil { - self.action.positionMode = .generic - } - - if self.action.anchor == nil { - self.action.anchor = .center - } - } - } -} diff --git a/Loop/Settings/Keybindings/Custom Keybinds/PreviewWindowButton.swift b/Loop/Settings/Keybindings/Custom Keybinds/PreviewWindowButton.swift deleted file mode 100644 index 2930b87b..00000000 --- a/Loop/Settings/Keybindings/Custom Keybinds/PreviewWindowButton.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// PreviewWindowButton.swift -// Loop -// -// Created by Kai Azim on 2023-12-25. -// - -import SwiftUI - -struct PreviewWindowButton: NSViewRepresentable { - @Binding var keybind: WindowAction - - init(_ keybind: Binding) { - self._keybind = keybind - } - - func makeNSView(context: NSViewRepresentableContext) -> NSButton { - let button = NSButton( - title: .init(localized: .init("Show", defaultValue: "Show")), - target: context.coordinator, - action: #selector(Coordinator.buttonClicked) - ) - - button.translatesAutoresizingMaskIntoConstraints = false - button.setContentHuggingPriority(.defaultHigh, for: .vertical) - button.setContentHuggingPriority(.defaultHigh, for: .horizontal) - button.sendAction(on: [.leftMouseDown, .leftMouseUp, .leftMouseDragged]) - - // Having this tracking area ensures that the preview window is closed even - // when the cursor is outside the button during button release. - let trackingArea = NSTrackingArea( - rect: button.bounds, - options: [.mouseEnteredAndExited, .activeAlways], - owner: context.coordinator, - userInfo: nil - ) - button.addTrackingArea(trackingArea) - - context.coordinator.parent = self - - return button - } - - func updateNSView(_ nsView: NSButton, context: NSViewRepresentableContext) { } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - static func dismantleNSView(_ nsView: NSView, coordinator _: Coordinator) { - nsView.trackingAreas.forEach { nsView.removeTrackingArea($0) } - } - - class Coordinator: NSResponder { - var parent: PreviewWindowButton - - let previewController = PreviewController() - - init(_ parent: PreviewWindowButton) { - self.parent = parent - super.init() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - @objc func buttonClicked() { - if NSApp.currentEvent?.type == .leftMouseUp { - self.previewController.close() - } else { - guard let screen = NSScreen.main else { return } - self.previewController.open(screen: screen, startingAction: self.parent.keybind) - } - } - - override func mouseExited(with _: NSEvent) { - self.previewController.close() - } - } -} diff --git a/Loop/Settings/Keybindings/Keybind Recorder/Keycorder.swift b/Loop/Settings/Keybindings/Keybind Recorder/Keycorder.swift deleted file mode 100644 index a6a0f8b6..00000000 --- a/Loop/Settings/Keybindings/Keybind Recorder/Keycorder.swift +++ /dev/null @@ -1,218 +0,0 @@ -// -// Keycorder.swift -// Loop -// -// Created by Kai Azim on 2023-11-10. -// - -import SwiftUI -import Defaults -import Carbon.HIToolbox - -struct Keycorder: View { - @EnvironmentObject private var keycorderModel: KeycorderModel - - let keyLimit: Int = 6 - - @Binding private var triggerKey: Set - @Binding private var validCurrentKeybind: Set - @State private var selectionKeybind: Set - @Binding private var direction: WindowDirection - - @State private var eventMonitor: NSEventMonitor? - @State private var shouldShake: Bool = false - @State private var shouldError: Bool = false - @State private var errorMessage: Text = Text("") // We use Text here for String interpolation with images - - @State private var isHovering: Bool = false - @State private var isActive: Bool = false - @State private var isCurrentlyPressed: Bool = false - - init(_ keybind: Binding, _ triggerKey: Binding>) { - self._validCurrentKeybind = keybind.keybind - self._direction = keybind.direction - self._triggerKey = triggerKey - self._selectionKeybind = State(initialValue: keybind.wrappedValue.keybind) - } - - let activeAnimation = Animation.easeInOut(duration: 0.5).repeatForever(autoreverses: true) - let noAnimation = Animation.linear(duration: 0) - - var body: some View { - Button { - guard !self.isActive else { return } - self.startObservingKeys() - } label: { - HStack(spacing: 5) { - if self.selectionKeybind.isEmpty { - Text( - self.isActive - ? .init(localized: .init("Press a key…", defaultValue: "Press a key…")) - : .init(localized: .init("None", defaultValue: "None")) - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(.horizontal, 8) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .foregroundStyle(.background) - RoundedRectangle(cornerRadius: 6) - .strokeBorder( - .tertiary.opacity((self.isHovering || self.isActive) ? 1 : 0.5), - lineWidth: 1 - ) - } - } - .fixedSize(horizontal: true, vertical: false) - } else { - ForEach(self.selectionKeybind.sorted(by: >), id: \.self) { key in - if let systemImage = key.systemImage { - Text("\(Image(systemName: systemImage))") - } else if let humanReadable = key.humanReadable { - Text(humanReadable) - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - .aspectRatio(1, contentMode: .fill) - .padding(5) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .foregroundStyle(.background) - RoundedRectangle(cornerRadius: 6) - .strokeBorder(.tertiary.opacity(self.isHovering ? 1 : 0.5), lineWidth: 1) - } - } - .fixedSize(horizontal: true, vertical: false) - } - } - .fontDesign(.monospaced) - .contentShape(Rectangle()) - } - .modifier(ShakeEffect(shakes: self.shouldShake ? 2 : 0)) - .animation(Animation.default, value: shouldShake) - .popover(isPresented: $shouldError, arrowEdge: .bottom) { - self.errorMessage - .multilineTextAlignment(.center) - .padding(8) - } - .onHover { hovering in - self.isHovering = hovering - } - .onChange(of: keycorderModel.eventMonitor) { _ in - if keycorderModel.eventMonitor != self.eventMonitor { - self.finishedObservingKeys(wasForced: true) - } - } - .onChange(of: self.validCurrentKeybind) { _ in - if self.selectionKeybind != self.validCurrentKeybind { - self.selectionKeybind = self.validCurrentKeybind - } - } - .buttonStyle(.plain) - .scaleEffect(self.isCurrentlyPressed ? 0.9 : 1) - } - - func startObservingKeys() { - self.selectionKeybind = [] - self.isActive = true - self.eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .keyUp, .flagsChanged]) { event in - if event.type == .flagsChanged { - if !Defaults[.triggerKey].contains(where: { $0.baseModifier == event.keyCode.baseModifier }) { - self.shouldError = false - self.selectionKeybind.insert(event.keyCode.baseModifier) - withAnimation(.snappy(duration: 0.1)) { - self.isCurrentlyPressed = true - } - } else { - if let systemImage = event.keyCode.baseModifier.systemImage { - // swiftlint:disable:next line_length - self.errorMessage = Text("\(Image(systemName: systemImage)) is already used as your trigger key.") - } else { - self.errorMessage = Text("That key is already used as your trigger key.") - } - - self.shouldShake.toggle() - self.shouldError = true - } - } - - if event.type == .keyUp || - (event.type == .flagsChanged && !self.selectionKeybind.isEmpty && event.modifierFlags.rawValue == 256) { - self.finishedObservingKeys() - return - } - - if event.type == .keyDown && !event.isARepeat { - if event.keyCode == CGKeyCode.kVK_Escape { - finishedObservingKeys(wasForced: true) - return - } - - if (self.selectionKeybind.count + self.triggerKey.count) >= keyLimit { - self.errorMessage = Text( - "You can only use up to \(keyLimit) keys in a keybind, including the trigger key." - ) - self.shouldShake.toggle() - self.shouldError = true - } else { - self.shouldError = false - self.selectionKeybind.insert(event.keyCode) - withAnimation(.snappy(duration: 0.1)) { - self.isCurrentlyPressed = true - } - } - - } - } - - self.eventMonitor!.start() - keycorderModel.eventMonitor = eventMonitor - } - - func finishedObservingKeys(wasForced: Bool = false) { - self.isActive = false - var willSet = !wasForced - - withAnimation(.snappy(duration: 0.1)) { - self.isCurrentlyPressed = false - } - - if self.validCurrentKeybind == self.selectionKeybind { - willSet = false - } - - if willSet { - for keybind in Defaults[.keybinds] where ( - keybind.keybind == self.selectionKeybind - ) { - willSet = false - if keybind.direction == .custom { - if let name = keybind.name { - self.errorMessage = Text("That keybind is already being used by \(name).") - } else { - self.errorMessage = Text("That keybind is already being used by another custom keybind.") - } - } else { - self.errorMessage = Text( - "That keybind is already being used by \(keybind.direction.name.lowercased())." - ) - } - self.shouldShake.toggle() - self.shouldError = true - break - } - } - - if willSet { - // Set the valid keybind to the current selected one - self.validCurrentKeybind = self.selectionKeybind - } else { - // Set preview keybind back to previous one - self.selectionKeybind = self.validCurrentKeybind - } - - self.eventMonitor?.stop() - self.eventMonitor = nil - } -} diff --git a/Loop/Settings/Keybindings/Keybind Recorder/KeycorderModel.swift b/Loop/Settings/Keybindings/Keybind Recorder/KeycorderModel.swift deleted file mode 100644 index 2b9afe2c..00000000 --- a/Loop/Settings/Keybindings/Keybind Recorder/KeycorderModel.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// KeycorderModel.swift -// Loop -// -// Created by Kai Azim on 2023-11-24. -// - -import SwiftUI - -class KeycorderModel: ObservableObject { - @Published var eventMonitor: NSEventMonitor? -} diff --git a/Loop/Settings/Keybindings/Keybind Recorder/TriggerKeycorder.swift b/Loop/Settings/Keybindings/Keybind Recorder/TriggerKeycorder.swift deleted file mode 100644 index 7d495e3d..00000000 --- a/Loop/Settings/Keybindings/Keybind Recorder/TriggerKeycorder.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// TriggerKeycorder.swift -// Loop -// -// Created by Kai Azim on 2023-09-11. -// - -import SwiftUI - -struct TriggerKeycorder: View { - @EnvironmentObject private var keycorderModel: KeycorderModel - - let keyLimit: Int = 5 - - @Binding private var validCurrentKey: Set - @State private var selectionKey: Set - - @State private var eventMonitor: NSEventMonitor? - @State private var shouldShake: Bool = false - @State private var isHovering: Bool = false - @State private var isActive: Bool = false - @State private var isCurrentlyPressed: Bool = false - @State private var tooManyKeysPopup: Bool = false - - init(_ key: Binding>) { - self._validCurrentKey = key - _selectionKey = State(initialValue: key.wrappedValue) - } - - var body: some View { - Button { - guard !self.isActive else { return } - self.startObservingKeys() - } label: { - HStack(spacing: 5) { - if self.selectionKey.isEmpty { - Text(self.isActive ? "Set a trigger key…" : "None") - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(5) - .padding(.horizontal, 8) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .foregroundStyle(.background) - RoundedRectangle(cornerRadius: 6) - .strokeBorder( - .tertiary.opacity((self.isHovering || self.isActive) ? 1 : 0.5), - lineWidth: 1 - ) - } - } - .fixedSize(horizontal: true, vertical: false) - } else { - ForEach(self.selectionKey.sorted(), id: \.self) { key in - // swiftlint:disable:next line_length - Text("\(key.isOnRightSide ? String(localized: .init("Right", defaultValue: "Right")) : String(localized: .init("Left", defaultValue: "Left"))) \(Image(systemName: key.systemImage ?? "exclamationmark.circle.fill"))") - .frame(maxWidth: .infinity, maxHeight: .infinity) - .padding(5) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .foregroundStyle(.background) - RoundedRectangle(cornerRadius: 6) - .strokeBorder(.tertiary.opacity(self.isHovering ? 1 : 0.5), lineWidth: 1) - } - } - .fixedSize(horizontal: true, vertical: false) - } - } - } - .fontDesign(.monospaced) - .contentShape(Rectangle()) - } - .modifier(ShakeEffect(shakes: self.shouldShake ? 2 : 0)) - .animation(Animation.default, value: shouldShake) - .popover(isPresented: $tooManyKeysPopup, arrowEdge: .bottom) { - Text("You can only use up to \(keyLimit) keys in your trigger key.") - .multilineTextAlignment(.center) - .padding(8) - } - .onHover { hovering in - self.isHovering = hovering - } - .onChange(of: keycorderModel.eventMonitor) { _ in - if keycorderModel.eventMonitor != self.eventMonitor { - self.finishedObservingKeys(wasForced: true) - } - } - .onChange(of: self.validCurrentKey) { _ in - if self.selectionKey != self.validCurrentKey { - self.selectionKey = self.validCurrentKey - } - } - .buttonStyle(.plain) - .scaleEffect(self.isCurrentlyPressed ? 0.9 : 1) - } - - func startObservingKeys() { - self.selectionKey = [] - self.isActive = true - self.eventMonitor = NSEventMonitor(scope: .local, eventMask: [.keyDown, .flagsChanged]) { event in - // keyDown event is only used to track escape key - if event.type == .keyDown && event.keyCode == CGKeyCode.kVK_Escape { - finishedObservingKeys(wasForced: true) - } - - if CGKeyCode.keyToImage.contains(where: { $0.key == event.keyCode.baseModifier }) { - self.selectionKey.insert(event.keyCode) - withAnimation(.snappy(duration: 0.1)) { - self.isCurrentlyPressed = true - } - } - - // Backup system in case keys are pressed at the exact same time - let flags = event.modifierFlags.convertToCGKeyCode() - if flags.count != selectionKey.count { - for key in flags where CGKeyCode.keyToImage.contains(where: { $0.key == key }) { - if !self.selectionKey.map({ $0.baseModifier }).contains(key) { - self.selectionKey.insert(key) - withAnimation(.snappy(duration: 0.1)) { - self.isCurrentlyPressed = true - } - } - } - } - - if event.modifierFlags.wasKeyUp && !self.selectionKey.isEmpty { - self.finishedObservingKeys() - return - } - - if !event.modifierFlags.wasKeyUp && self.selectionKey.isEmpty { - self.shouldShake.toggle() - } - } - - self.eventMonitor!.start() - keycorderModel.eventMonitor = eventMonitor - } - - func finishedObservingKeys(wasForced: Bool = false) { - var willSet = !wasForced - - if self.selectionKey.count > self.keyLimit { - willSet = false - self.shouldShake.toggle() - self.tooManyKeysPopup = true - } - - self.isActive = false - withAnimation(.snappy(duration: 0.1)) { - self.isCurrentlyPressed = false - } - - if willSet { - // Set the valid keybind to the current selected one - self.validCurrentKey = selectionKey - } else { - // Set preview keybind back to previous one - self.selectionKey = self.validCurrentKey - } - - self.eventMonitor?.stop() - self.eventMonitor = nil - } -} diff --git a/Loop/Settings/Keybindings/KeybindCustomizationViewItem.swift b/Loop/Settings/Keybindings/KeybindCustomizationViewItem.swift deleted file mode 100644 index 9f9d5190..00000000 --- a/Loop/Settings/Keybindings/KeybindCustomizationViewItem.swift +++ /dev/null @@ -1,180 +0,0 @@ -// -// KeybindCustomizationViewItem.swift -// Loop -// -// Created by Kai Azim on 2023-10-31. -// - -import SwiftUI -import Defaults - -struct KeybindCustomizationViewItem: View { - @Binding var keybind: WindowAction - @Binding var triggerKey: Set - @State var showingInfo: Bool = false - @State var isConfiguringCustomKeybind: Bool = false - @State var isConfiguringCyclingKeybind: Bool = false - - var body: some View { - HStack { - directionPicker(selection: $keybind.direction) - - if let moreInformation = self.keybind.direction.moreInformation { - Button(action: { - self.showingInfo.toggle() - }, label: { - Image(systemName: "info.circle") - .font(.title3) - .foregroundStyle(.secondary) - }) - .buttonStyle(.plain) - .popover(isPresented: $showingInfo, arrowEdge: .bottom) { - Text(moreInformation) - .multilineTextAlignment(.center) - .padding(8) - } - } - - if self.keybind.direction == .custom { - Button(action: { - self.isConfiguringCustomKeybind.toggle() - }, label: { - Image(systemName: "slider.horizontal.3") - .font(.title3) - .foregroundStyle(.secondary) - }) - .buttonStyle(.plain) - .sheet(isPresented: self.$isConfiguringCustomKeybind) { - CustomKeybindView(action: $keybind, isSheetShown: $isConfiguringCustomKeybind) - } - } - - if self.keybind.direction == .cycle { - Button(action: { - self.isConfiguringCyclingKeybind.toggle() - }, label: { - Image(systemName: "slider.horizontal.3") - .font(.title3) - .foregroundStyle(.secondary) - }) - .buttonStyle(.plain) - .sheet(isPresented: self.$isConfiguringCyclingKeybind) { - CustomCyclingKeybindView(action: $keybind, isSheetShown: $isConfiguringCyclingKeybind) - } - } - - Spacer() - - Group { - ForEach(self.triggerKey.sorted(), id: \.self) { key in - Text("\(Image(systemName: key.systemImage ?? "exclamationmark.circle.fill"))") - .foregroundStyle(.secondary) - .fontDesign(.monospaced) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .aspectRatio(1, contentMode: .fill) - .padding(5) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .foregroundStyle(.background.opacity(0.8)) - RoundedRectangle(cornerRadius: 6) - .strokeBorder(.tertiary.opacity(0.5), lineWidth: 1) - } - .opacity(0.8) - } - .fixedSize(horizontal: true, vertical: false) - } - - Image(systemName: "plus") - .foregroundStyle(.secondary) - .fontDesign(.monospaced) - - Keycorder($keybind, $triggerKey) - } - } - .padding(.vertical, 5) - } - - var directionPickerList: some View { - Group { - Picker("General", selection: $keybind.direction) { - ForEach(WindowDirection.general) { direction in - directionPickerItem(direction) - } - } - Picker("Halves", selection: $keybind.direction) { - ForEach(WindowDirection.halves) { direction in - directionPickerItem(direction) - } - } - Picker("Quarters", selection: $keybind.direction) { - ForEach(WindowDirection.quarters) { direction in - directionPickerItem(direction) - } - } - Picker("Horizontal Thirds", selection: $keybind.direction) { - ForEach(WindowDirection.horizontalThirds) { direction in - directionPickerItem(direction) - } - } - Picker("Vertical Thirds", selection: $keybind.direction) { - ForEach(WindowDirection.verticalThirds) { direction in - directionPickerItem(direction) - } - } - Picker("Screen Switching", selection: $keybind.direction) { - ForEach(WindowDirection.screenSwitching) { direction in - directionPickerItem(direction) - } - } - Picker("Grow/Shrink", selection: $keybind.direction) { - ForEach(WindowDirection.sizeAdjustment) { direction in - directionPickerItem(direction) - } - Divider() - ForEach(WindowDirection.shrink) { direction in - directionPickerItem(direction) - } - Divider() - ForEach(WindowDirection.grow) { direction in - directionPickerItem(direction) - } - } - Picker("More", selection: $keybind.direction) { - ForEach(WindowDirection.more) { direction in - directionPickerItem(direction) - } - } - } - } - - @ViewBuilder - func directionPicker(selection: Binding) -> some View { - Menu(content: { - directionPickerList - }, label: { - HStack { - keybind.direction.icon - - if keybind.direction == .custom { - Text(keybind.name ?? .init(localized: .init("Custom Keybind", defaultValue: "Custom Keybind"))) - } else if keybind.direction == .cycle { - Text(keybind.name ?? .init(localized: .init("Custom Cycle", defaultValue: "Custom Cycle"))) - } else { - Text(keybind.direction.name) - } - - } - }) - .fixedSize() - } - - @ViewBuilder - func directionPickerItem(_ direction: WindowDirection) -> some View { - HStack { - direction.icon - Text(direction.name) - } - .tag(direction) - } -} diff --git a/Loop/Settings/Keybindings/KeybindingsSettingsView.swift b/Loop/Settings/Keybindings/KeybindingsSettingsView.swift deleted file mode 100644 index 4ff02f65..00000000 --- a/Loop/Settings/Keybindings/KeybindingsSettingsView.swift +++ /dev/null @@ -1,273 +0,0 @@ -// -// KeybindingsSettingsView.swift -// Loop -// -// Created by Kai Azim on 2023-10-28. -// - -import SwiftUI -import Defaults - -struct KeybindingsSettingsView: View { - @Default(.keybinds) var keybinds - @Default(.useSystemAccentColor) var useSystemAccentColor - @Default(.customAccentColor) var customAccentColor - - @Default(.triggerKey) var triggerKey - @Default(.doubleClickToTrigger) var doubleClickToTrigger - @Default(.triggerDelay) var triggerDelay - @Default(.middleClickTriggersLoop) var middleClickTriggersLoop - - @StateObject private var keycorderModel = KeycorderModel() - @State private var suggestAddingTriggerDelay: Bool = false - @State private var selection = Set() - - var body: some View { - ZStack { - Form { - Section("Trigger Key") { - VStack(alignment: .leading) { - HStack { - Text("Trigger key") - Spacer() - TriggerKeycorder(self.$triggerKey) - } - - if triggerKey == [.kVK_RightControl] { - Text("Tip: To use Caps Lock, remap it to control in System Settings!") - .font(.caption) - .foregroundColor(.secondary) - } - } - - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Trigger Delay", defaultValue: "Trigger delay")), - value: $triggerDelay, - sliderRange: 0...1, - postscript: .init(localized: .init("sec", defaultValue: "sec")), - step: 0.1, - lowerClamp: true - ) - - Toggle("Double-click trigger key to trigger \(Bundle.main.appName)", isOn: $doubleClickToTrigger) - Toggle("Middle-click to trigger \(Bundle.main.appName)", isOn: $middleClickTriggersLoop) - } - - Section { - VStack(spacing: 0) { - if self.keybinds.isEmpty { - HStack { - Spacer() - VStack { - Text("No keybinds") - .font(.title3) - Text("Press + to add a keybind!") - .font(.caption) - } - Spacer() - } - .foregroundStyle(.secondary) - .padding() - } else { - List(selection: $selection) { - ForEach(self.$keybinds) { keybind in - KeybindCustomizationViewItem(keybind: keybind, triggerKey: self.$triggerKey) - .contextMenu { - Button("Delete") { - if self.selection.isEmpty { - self.keybinds.removeAll { - $0 == keybind.wrappedValue - } - } else { - for item in selection { - self.keybinds.removeAll { - $0 == item - } - } - self.selection.removeAll() - } - } - } - .tag(keybind.wrappedValue) - .fixedSize(horizontal: false, vertical: true) - } - .onMove { indices, newOffset in - self.keybinds.move(fromOffsets: indices, toOffset: newOffset) - } - .onDelete { offset in - self.keybinds.remove(atOffsets: offset) - } - } - .listStyle(.bordered(alternatesRowBackgrounds: true)) - } - - Divider() - - Rectangle() - .frame(height: 20) - .foregroundStyle(.quinary) - .overlay { - HStack(spacing: 5) { - Menu(content: { - newDirectionMenu() - }, label: { - Rectangle() - .foregroundStyle(.white.opacity(0.00001)) - .overlay { - Image(systemName: "plus") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - } - .aspectRatio(1, contentMode: .fit) - .padding(-5) - }) - - Divider() - - Button { - for item in selection { - self.keybinds.removeAll { - $0 == item - } - } - self.selection.removeAll() - } label: { - Rectangle() - .foregroundStyle(.white.opacity(0.00001)) - .overlay { - Image(systemName: "minus") - .font(.footnote) - .fontWeight(.semibold) - .foregroundStyle(.secondary) - } - .aspectRatio(1, contentMode: .fit) - .padding(-5) - } - .disabled(self.selection.isEmpty) - - Spacer() - } - .buttonStyle(.plain) - .padding(5) - } - } - .ignoresSafeArea() - .padding(-10) - } header: { - Text("Keybinds") - } footer: { - HStack { - Button("Import", systemImage: "square.and.arrow.down") { - WindowAction.importPrompt() - } - - Button("Export", systemImage: "square.and.arrow.up") { - WindowAction.exportPrompt() - } - - Button("Restore Defaults", systemImage: "arrow.counterclockwise") { - _keybinds.reset() - _triggerKey.reset() - _doubleClickToTrigger.reset() - _triggerDelay.reset() - _middleClickTriggersLoop.reset() - keycorderModel.eventMonitor = nil - } - } - .padding(.top, 10) - } - } - .formStyle(.grouped) - } - .environmentObject(keycorderModel) - } - - @ViewBuilder - func newDirectionMenu() -> some View { - Menu("General") { - ForEach(WindowDirection.general) { direction in - newDirectionButton(direction) - } - } - - Menu("Halves") { - ForEach(WindowDirection.halves) { direction in - newDirectionButton(direction) - } - } - - Menu("Quarters") { - ForEach(WindowDirection.quarters) { direction in - newDirectionButton(direction) - } - } - - Menu("Horizontal Thirds") { - ForEach(WindowDirection.horizontalThirds) { direction in - newDirectionButton(direction) - } - } - - Menu("Vertical Thirds") { - ForEach(WindowDirection.verticalThirds) { direction in - newDirectionButton(direction) - } - } - - Menu("Screen Switching") { - ForEach(WindowDirection.screenSwitching) { direction in - newDirectionButton(direction) - } - } - - Menu("Grow/Shrink") { - ForEach(WindowDirection.sizeAdjustment) { direction in - newDirectionButton(direction) - } - - Divider() - - ForEach(WindowDirection.shrink) { direction in - newDirectionButton(direction) - } - - Divider() - - ForEach(WindowDirection.grow) { direction in - newDirectionButton(direction) - } - } - - Menu("More") { - ForEach(WindowDirection.more) { direction in - newDirectionButton(direction) - } - } - } - - @ViewBuilder - func newDirectionButton(_ direction: WindowDirection) -> some View { - Button(action: { - if direction == .custom { - self.keybinds.append( - WindowAction( - .custom, - keybind: [], - unit: .percentage, - anchor: .center, - positionMode: .generic, - sizeMode: .custom - ) - ) - } else { - self.keybinds.append(WindowAction(direction, keybind: [])) - } - }, label: { - HStack { - direction.icon - Text(direction.name) - } - }) - } -} diff --git a/Loop/Settings/MoreSettingsView.swift b/Loop/Settings/MoreSettingsView.swift deleted file mode 100644 index 97771e03..00000000 --- a/Loop/Settings/MoreSettingsView.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// MoreSettingsView.swift -// Loop -// -// Created by Kai Azim on 2023-01-28. -// - -import SwiftUI -import Sparkle -import Defaults - -struct MoreSettingsView: View { - @Environment(\.openURL) private var openURL - @EnvironmentObject var updater: SoftwareUpdater - - @Default(.respectStageManager) var respectStageManager - @Default(.stageStripSize) var stageStripSize - @Default(.hapticFeedback) var hapticFeedback - @Default(.hideUntilDirectionIsChosen) var hideUntilDirectionIsChosen - @Default(.sizeIncrement) var sizeIncrement - @Default(.animateWindowResizes) var animateWindowResizes - @Default(.includeDevelopmentVersions) var includeDevelopmentVersions - @State var isAccessibilityAccessGranted = false - - var body: some View { - Form { - Section(content: { - Toggle("Automatically check for updates", isOn: $updater.automaticallyChecksForUpdates) - Toggle("Include development versions", isOn: $includeDevelopmentVersions) - }, header: { - HStack { - VStack(alignment: .leading, spacing: 0) { - Text("Updates") - Button(action: { - let pasteboard = NSPasteboard.general - pasteboard.clearContents() - pasteboard.setString( - "Version \(Bundle.main.appVersion) (\(Bundle.main.appBuild))", - forType: NSPasteboard.PasteboardType.string - ) - }, label: { - let versionText = String( - localized: "Current version: \(Bundle.main.appVersion) (\(Bundle.main.appBuild))" - ) - HStack { - Text("\(versionText) \(Image(systemName: "doc.on.clipboard"))") - .font(.caption) - .foregroundColor(.secondary) - } - }) - .buttonStyle(.plain) - } - - Spacer() - - Button("Check for Updates…") { - updater.checkForUpdates() - } - .buttonStyle(.link) - .foregroundStyle(Color.accentColor) - } - }) - - Section("Stage Manager") { - Toggle("Respect Stage Manager", isOn: $respectStageManager) - - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Stage Strip Size", defaultValue: "Stage strip size")), - value: $stageStripSize, - sliderRange: 50...200, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - .disabled(!respectStageManager) - } - - Section("Advanced") { - Toggle(isOn: $animateWindowResizes) { - HStack { - Text("Animate windows being resized") - UnstableIndicator(.init(localized: .init("ALPHA", defaultValue: "ALPHA")), color: .orange) - } - } - - Toggle("Hide menu until direction is chosen", isOn: $hideUntilDirectionIsChosen) - - Toggle("Haptic feedback", isOn: $hapticFeedback) - - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Size Increment", defaultValue: "Size increment")), - description: .init( - localized: .init( - "Crisp Value Adjuster: Size Increment Description", - defaultValue: "Used in size adjustment window actions" - ) - ), - value: $sizeIncrement, - sliderRange: 5...50, - postscript: .init(localized: .init("px", defaultValue: "px")), - step: 4.5, - lowerClamp: true - ) - } - - Section(content: { - HStack { - Text("Accessibility access") - Spacer() - Text( - isAccessibilityAccessGranted - ? .init(localized: .init("Granted", defaultValue: "Granted")) - : .init(localized: .init("Not granted", defaultValue: "Not granted")) - ) - Circle() - .frame(width: 8, height: 8) - .padding(.trailing, 5) - .foregroundColor(isAccessibilityAccessGranted ? .green : .red) - .shadow(color: isAccessibilityAccessGranted ? .green : .red, radius: 8) - } - }, header: { - HStack { - Text("Permissions") - - Spacer() - - Button("Request Access…") { - AccessibilityManager.requestAccess() - self.isAccessibilityAccessGranted = AccessibilityManager.getStatus() - } - .buttonStyle(.link) - .foregroundStyle(Color.accentColor) - .disabled(isAccessibilityAccessGranted) - .opacity(isAccessibilityAccessGranted ? 0.6 : 1) - .onAppear { - self.isAccessibilityAccessGranted = AccessibilityManager.getStatus() - } - } - }) - - Section("Feedback") { - HStack { - Text(""" -Sending feedback will bring you to our \"New Issue\" GitHub page, where you can report a bug, request a feature & more! -""") - .font(.caption) - .foregroundStyle(.secondary) - - Spacer() - - Button(action: { - openURL(URL(string: "https://github.com/MrKai77/Loop/issues/new/choose")!) - }, label: { - Text("Send Feedback") - }) - .controlSize(.large) - } - } - } - .formStyle(.grouped) - .scrollDisabled(true) - } -} diff --git a/Loop/Settings/Padding/PaddingConfigurationView.swift b/Loop/Settings/Padding/PaddingConfigurationView.swift deleted file mode 100644 index 8cfac512..00000000 --- a/Loop/Settings/Padding/PaddingConfigurationView.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// PaddingConfigurationView.swift -// Loop -// -// Created by Kai Azim on 2024-02-01. -// - -import SwiftUI - -struct PaddingConfigurationView: View { - @Binding var isSheetShown: Bool - @Binding var paddingModel: PaddingModel - - var body: some View { - VStack { - Form { - Section("Padding") { - Toggle("Custom screen padding", isOn: $paddingModel.configureScreenPadding) - } - - Section(content: { - ZStack { - WallpaperView().equatable() - PaddingPreviewView($paddingModel) - } - .ignoresSafeArea() - .padding(-10) - .aspectRatio(16/10, contentMode: .fit) - }, footer: { - HStack { - Text("This preview is not to scale.") - .font(.caption) - .foregroundStyle(.secondary) - Spacer() - } - }) - - if paddingModel.configureScreenPadding { - Section { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Window Gaps", defaultValue: "Window gaps")), - value: $paddingModel.window, - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: External Bar", defaultValue: "External bar")), - description: .init( - localized: .init( - "Crisp Value Adjuster: External Bar Description", - defaultValue: "Use this if you are using a custom menubar." - ) - ), - value: $paddingModel.externalBar, - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - } - - Section("Screen Padding") { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Top", defaultValue: "Top")), - value: $paddingModel.top, - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Bottom", defaultValue: "Bottom")), - value: $paddingModel.bottom, - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Right", defaultValue: "Right")), - value: $paddingModel.right, - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Left", defaultValue: "Left")), - value: $paddingModel.left, - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - } - } else { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Padding", defaultValue: "Padding")), - value: Binding( - get: { - paddingModel.window - }, - set: { - paddingModel.window = $0 - paddingModel.top = $0 - paddingModel.bottom = $0 - paddingModel.right = $0 - paddingModel.left = $0 - } - ), - sliderRange: 0...100, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true - ) - } - } - .formStyle(.grouped) - .scrollDisabled(true) - .onChange(of: paddingModel.configureScreenPadding) { _ in - if !paddingModel.configureScreenPadding { - paddingModel.top = paddingModel.window - paddingModel.externalBar = 0 - paddingModel.bottom = paddingModel.window - paddingModel.right = paddingModel.window - paddingModel.left = paddingModel.window - } - } - - HStack { - Button { - isSheetShown = false - } label: { - Text("Done") - } - .controlSize(.large) - } - .offset(y: -14) - } - .frame(width: 400) - .fixedSize(horizontal: false, vertical: true) - .background(.background) - } -} diff --git a/Loop/Settings/PreviewSettingsView.swift b/Loop/Settings/PreviewSettingsView.swift deleted file mode 100644 index 01a42b5e..00000000 --- a/Loop/Settings/PreviewSettingsView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// PreviewSettingsView.swift -// Loop -// -// Created by Kai Azim on 2023-01-25. -// - -import SwiftUI -import Defaults - -struct PreviewSettingsView: View { - @Default(.previewVisibility) var previewVisibility - @Default(.previewPadding) var previewPadding - @Default(.previewCornerRadius) var previewCornerRadius - @Default(.previewBorderThickness) var previewBorderThickness - @Default(.animateWindowResizes) var animateWindowResizes - - var body: some View { - Form { - Section("Appearance") { - Toggle(isOn: $previewVisibility) { - VStack(alignment: .leading) { - Text("Show preview when looping") - - if !previewVisibility { - VStack(alignment: .leading) { - Text("Adjusts window frame in real-time as you choose a direction.") - - if self.animateWindowResizes { - Text("Windows will not animate their resizes.") - } - } - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - } - - Section { - ZStack { - VisualEffectView(material: .sidebar, blendingMode: .behindWindow) - .ignoresSafeArea() - .padding(-10) - - PreviewView(previewMode: true) - } - } - .frame(height: 150) - .opacity(previewVisibility ? 1 : 0.5) - - Section { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Padding", defaultValue: "Padding")), - value: $previewPadding, - sliderRange: 0...20, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true, - upperClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Corner Radius", defaultValue: "Corner radius")), - value: $previewCornerRadius, - sliderRange: 0...20, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true, - upperClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Border Thickness", defaultValue: "Border thickness")), - value: $previewBorderThickness, - sliderRange: 0...10, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true, - upperClamp: true - ) - } - .disabled(!previewVisibility) - .foregroundColor(!previewVisibility ? .secondary : nil) - } - .formStyle(.grouped) - .scrollDisabled(true) - } -} diff --git a/Loop/Settings/RadialMenuSettingsView.swift b/Loop/Settings/RadialMenuSettingsView.swift deleted file mode 100644 index 2aaf92e6..00000000 --- a/Loop/Settings/RadialMenuSettingsView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// RadialMenuSettingsView.swift -// Loop -// -// Created by Kai Azim on 2023-01-25. -// - -import SwiftUI -import Defaults - -struct RadialMenuSettingsView: View { - @Default(.radialMenuVisibility) var radialMenuVisibility - @Default(.radialMenuCornerRadius) var radialMenuCornerRadius - @Default(.radialMenuThickness) var radialMenuThickness - @Default(.disableCursorInteraction) var disableCursorInteraction - - var body: some View { - Form { - Section("Appearance") { - Toggle("Show radial menu when looping", isOn: $radialMenuVisibility) - - Toggle("Disable cursor interaction", isOn: $disableCursorInteraction) - .disabled(!radialMenuVisibility) - .foregroundColor(!radialMenuVisibility ? .secondary : nil) - } - - Section { - ZStack { - VisualEffectView(material: .sidebar, blendingMode: .behindWindow) - .ignoresSafeArea() - .padding(-10) - - RadialMenuView( - previewMode: true, - window: nil - ) - } - } - .opacity(radialMenuVisibility ? 1 : 0.5) - - Section { - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Corner Radius", defaultValue: "Corner radius")), - value: Binding( - get: { - radialMenuCornerRadius - }, - set: { - radialMenuCornerRadius = $0 - radialMenuThickness = min(radialMenuThickness, radialMenuCornerRadius - 1) - } - ), - sliderRange: 30...50, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true, - upperClamp: true - ) - CrispValueAdjuster( - .init(localized: .init("Crisp Value Adjuster: Thickness", defaultValue: "Thickness")), - value: Binding( - get: { - radialMenuThickness - }, - set: { - radialMenuThickness = $0 - radialMenuCornerRadius = max(radialMenuThickness + 1, radialMenuCornerRadius) - } - ), - sliderRange: 10...35, - postscript: .init(localized: .init("px", defaultValue: "px")), - lowerClamp: true, - upperClamp: true - ) - } - .disabled(!radialMenuVisibility) - .foregroundColor(!radialMenuVisibility ? .secondary : nil) - } - .formStyle(.grouped) - .scrollDisabled(true) - } -} diff --git a/Loop/Settings/SettingsView.swift b/Loop/Settings/SettingsView.swift deleted file mode 100644 index fdcab43a..00000000 --- a/Loop/Settings/SettingsView.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// SettingsView.swift -// Loop -// -// Created by Kai Azim on 2023-11-30. -// - -import SwiftUI -import Sparkle - -struct SettingsView: View { - @State var currentSettingsTab = SettingsTab.general - @StateObject private var updater = SoftwareUpdater() - private var appListManager = AppListManager() - - var body: some View { - TabView(selection: $currentSettingsTab) { - GeneralSettingsView() - .tag(SettingsTab.general) - .tabItem { - Image(systemName: "gear") - Text("General") - } - .frame(width: 450) - - RadialMenuSettingsView() - .tag(SettingsTab.radialMenu) - .tabItem { - Image(.loop) - Text("Radial Menu") - } - .frame(width: 450) - - PreviewSettingsView() - .tag(SettingsTab.preview) - .tabItem { - Image(systemName: "rectangle.portrait.and.arrow.right") - Text("Preview") - } - .frame(width: 450) - - KeybindingsSettingsView() - .tag(SettingsTab.keybindings) - .tabItem { - Image(systemName: "keyboard") - Text("Keybindings") - } - .frame(width: 500) - .frame(minHeight: 500, maxHeight: 680) - - ExcludeListSettingsView() - .tag(SettingsTab.excludedApps) - .tabItem { - Image(systemName: "xmark.app") - Text("Excluded Apps") - } - .environmentObject(appListManager) - .frame(width: 450) - .frame(maxHeight: 680) - - MoreSettingsView() - .tag(SettingsTab.more) - .tabItem { - Image(systemName: "ellipsis.circle") - Text("More") - } - .environmentObject(updater) - .frame(width: 450) - } - .fixedSize(horizontal: true, vertical: true) - } - - enum SettingsTab: Int { - case general - case radialMenu - case preview - case keybindings - case excludedApps - case more - } -} diff --git a/Loop/Utilities/AnimationConfiguration.swift b/Loop/Utilities/AnimationConfiguration.swift index 4e1d6d60..ea6ee346 100644 --- a/Loop/Utilities/AnimationConfiguration.swift +++ b/Loop/Utilities/AnimationConfiguration.swift @@ -5,24 +5,24 @@ // Created by Kai Azim on 2023-10-27. // -import SwiftUI import Defaults +import SwiftUI enum AnimationConfiguration: Int, Defaults.Serializable, CaseIterable, Identifiable { - var id: Self { return self } + var id: Self { self } case smooth = 0 case fast = 1 case instant = 2 - var name: String { + var name: LocalizedStringKey { switch self { case .smooth: - .init(localized: .init("Smooth", defaultValue: "Smooth")) + "Smooth" case .fast: - .init(localized: .init("Fast", defaultValue: "Fast")) + "Fast" case .instant: - .init(localized: .init("Instant", defaultValue: "Instant")) + "Instant" } } @@ -37,6 +37,17 @@ enum AnimationConfiguration: Int, Defaults.Serializable, CaseIterable, Identifia } } + var previewTimingFunctionSwiftUI: Animation? { + switch self { + case .smooth: + Animation.timingCurve(0, 0.26, 0.45, 1) + case .fast: + Animation.timingCurve(0.22, 1, 0.47, 1) + case .instant: + nil + } + } + var radialMenuSize: Animation { switch self { case .smooth: diff --git a/Loop/Utilities/CrispValueAdjuster.swift b/Loop/Utilities/CrispValueAdjuster.swift deleted file mode 100644 index 9c805e37..00000000 --- a/Loop/Utilities/CrispValueAdjuster.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// CrispValueAdjuster.swift -// Loop -// -// Created by Kai Azim on 2024-02-15. -// - -import SwiftUI - -struct CrispValueAdjuster: View where V: Strideable, V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { - let title: String - let description: String? - @Binding var value: V - let sliderRange: ClosedRange - let postscript: String? - var step: V.Stride - let upperClamp: Bool - let lowerClamp: Bool - - @State var isPopoverShown: Bool = false - - init( - _ title: String, - description: String? = nil, - value: Binding, - sliderRange: ClosedRange, - postscript: String? = nil, - step: V? = nil, - lowerClamp: Bool = false, - upperClamp: Bool = false - ) { - self.title = title - self.description = description - self._value = value - self.sliderRange = sliderRange - self.postscript = postscript - self.lowerClamp = lowerClamp - self.upperClamp = upperClamp - - self.formatter = NumberFormatter() - self.formatter.maximumFractionDigits = 10 - - if let step = step { - self.step = V.Stride(step) - } else { - self.step = 0 // Initialize first - self.step = totalRange / 10 - } - } - - let stepperWidth: CGFloat = 150 - let formatter: NumberFormatter - - var totalRange: V.Stride { - V.Stride(sliderRange.upperBound) - V.Stride(sliderRange.lowerBound) - } - - var popoverXOffset: CGFloat { - 4 + (stepperWidth - 8) * ( - CGFloat( - value.clamped(to: sliderRange) - sliderRange.lowerBound - ) / CGFloat(totalRange) - ) - } - - var body: some View { - VStack(alignment: .leading) { - HStack { - Text(title) - - Spacer() - - HStack { - stepperMinText - - Slider( - value: $value, - in: sliderRange, - step: totalRange / 10, - label: { EmptyView() }, - onEditingChanged: { self.isPopoverShown = !$0 } - ) - .labelsHidden() - .frame(width: stepperWidth) - .popover( - isPresented: $isPopoverShown, - attachmentAnchor: .rect( - .rect( - CGRect( - // This compensates for the small spacing within the slider - x: popoverXOffset, - y: 0, - width: 0, - height: 0 - ) - ) - ), - arrowEdge: .top - ) { - stepperView - .padding(10) - .fixedSize() - } - - stepperMaxText - } - .fixedSize() - } - - if let description = description { - Text(description) - .font(.caption) - .foregroundStyle(.secondary) - } - } - } - - @ViewBuilder - var stepperView: some View { - HStack { - TextField( - .init(""), - value: Binding( - get: { - self.value - }, - set: { - if lowerClamp && upperClamp { - self.value = $0.clamped(to: sliderRange) - } else if lowerClamp { - self.value = max(self.sliderRange.lowerBound, $0) - } else if upperClamp { - self.value = min(self.sliderRange.upperBound, $0) - } else { - self.value = $0 - } - } - ), - formatter: formatter - ) - .labelsHidden() - .textFieldStyle(.plain) - .padding(4) - .padding(.trailing, 12) - .background { - ZStack { - RoundedRectangle(cornerRadius: 6) - .foregroundStyle(.background) - RoundedRectangle(cornerRadius: 6) - .strokeBorder( - .tertiary.opacity(0.5), - lineWidth: 1 - ) - } - } - .frame(minWidth: 20, maxWidth: 500) - .overlay { - HStack { - Spacer() - - Stepper( - .init(""), - value: Binding( - get: { - self.value - }, - set: { - if lowerClamp && upperClamp { - self.value = $0.clamped(to: sliderRange) - } else if lowerClamp { - self.value = max(self.sliderRange.lowerBound, $0) - } else if upperClamp { - self.value = min(self.sliderRange.upperBound, $0) - } else { - self.value = $0 - } - } - ), - step: step - ) - .labelsHidden() - } - .padding(.horizontal, 1) - } - .fixedSize() - .padding(.vertical, -10) - - if let postfix = postscript { - Text(postfix) - } - } - } - - @ViewBuilder - var stepperMinText: some View { - Text("\(sliderRange.lowerBound.formatted())\(postscript == nil ? "" : " \(postscript!)")") - .font(.caption2) - .foregroundStyle(.secondary) - .padding(.trailing, -4) - } - - @ViewBuilder - var stepperMaxText: some View { - Text("\(sliderRange.upperBound.formatted())\(postscript == nil ? "" : " \(postscript!)")") - .font(.caption2) - .foregroundStyle(.secondary) - .padding(.leading, -4) - } -} - -extension Comparable { - fileprivate func clamped(to limits: ClosedRange) -> Self { - return min(max(self, limits.lowerBound), limits.upperBound) - } -} diff --git a/Loop/Utilities/EventMonitor.swift b/Loop/Utilities/EventMonitor.swift index 92739b7c..17990e88 100644 --- a/Loop/Utilities/EventMonitor.swift +++ b/Loop/Utilities/EventMonitor.swift @@ -19,7 +19,7 @@ class NSEventMonitor: EventMonitor, Identifiable, Equatable { private var scope: NSEventMonitor.Scope private var eventTypeMask: NSEvent.EventTypeMask - private var eventHandler: (NSEvent) -> Void + private var eventHandler: (NSEvent) -> () var isEnabled: Bool = false enum Scope { @@ -32,43 +32,43 @@ class NSEventMonitor: EventMonitor, Identifiable, Equatable { stop() } - init(scope: Scope, eventMask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> Void) { + init(scope: Scope, eventMask: NSEvent.EventTypeMask, handler: @escaping (NSEvent) -> ()) { self.eventTypeMask = eventMask self.eventHandler = handler self.scope = scope } - public func start() { - if self.scope == .local || self.scope == .all { - self.localEventMonitor = NSEvent.addLocalMonitorForEvents( + func start() { + if scope == .local || scope == .all { + localEventMonitor = NSEvent.addLocalMonitorForEvents( matching: eventTypeMask ) { event in self.eventHandler(event) return nil } as AnyObject - self.isEnabled = true + isEnabled = true } - if self.scope == .global || self.scope == .all { - self.globalEventMonitor = NSEvent.addGlobalMonitorForEvents( + if scope == .global || scope == .all { + globalEventMonitor = NSEvent.addGlobalMonitorForEvents( matching: eventTypeMask, handler: eventHandler ) as AnyObject - self.isEnabled = true + isEnabled = true } } - public func stop() { + func stop() { if let localEventMonitor { NSEvent.removeMonitor(localEventMonitor) - self.isEnabled = false + isEnabled = false } if let globalEventMonitor { NSEvent.removeMonitor(globalEventMonitor) - self.isEnabled = false + isEnabled = false } } @@ -99,9 +99,9 @@ class CGEventMonitor: EventMonitor, Identifiable, Equatable { userInfo: Unmanaged.passRetained(self).toOpaque() ) - if let eventTap = self.eventTap { + if let eventTap { self.runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0) - if let runLoopSource = self.runLoopSource { + if let runLoopSource { CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes) } } else { @@ -114,21 +114,21 @@ class CGEventMonitor: EventMonitor, Identifiable, Equatable { } private func handleEvent(event: CGEvent) -> Unmanaged? { - return self.eventCallback(event) + eventCallback(event) } func start() { - if let eventTap = self.eventTap { + if let eventTap { CGEvent.tapEnable(tap: eventTap, enable: true) } - self.isEnabled = true + isEnabled = true } func stop() { - if let eventTap = self.eventTap { + if let eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) } - self.isEnabled = false + isEnabled = false } var id = UUID() diff --git a/Loop/Utilities/Icon.swift b/Loop/Utilities/Icon.swift new file mode 100644 index 00000000..7bd2f2fd --- /dev/null +++ b/Loop/Utilities/Icon.swift @@ -0,0 +1,155 @@ +// +// Icon.swift +// Loop +// +// Created by Kai Azim on 2024-06-07. +// + +import Luminare +import SwiftUI + +struct Icon: Hashable, LuminarePickerData { + var name: String + var iconName: String + var unlockTime: Int + var unlockMessage: String? + + var selectable: Bool { + IconManager.returnUnlockedIcons().contains(self) + } + + #if DEBUG + static let all: [Icon] = [ + .developer, + .classic, + .holo, + .rosePine, + .metaLoop, + .keycap, + .white, + .black, + .simon, + .neon, + .synthwaveSunset, + .blackHole, + .summer, + .master + ] + #else + static let all: [Icon] = [ + .classic, + .holo, + .rosePine, + .metaLoop, + .keycap, + .white, + .black, + .simon, + .neon, + .synthwaveSunset, + .blackHole, + .summer, + .master + ] + #endif +} + +// MARK: - Kai Azim + +extension Icon { + static let classic = Icon( + name: .init(localized: .init("Icon Name: Classic", defaultValue: "Classic")), + iconName: "AppIcon-Classic", + unlockTime: 0 + ) + static let holo = Icon( + name: .init(localized: .init("Icon Name: Holo", defaultValue: "Holo")), + iconName: "AppIcon-Holo", + unlockTime: 25, + unlockMessage: .init( + localized: .init( + "Icon Unlock Message: Holo", + defaultValue: """ + You've already looped 25 times! As a reward, here's new icon: \(.init(localized: .init("Icon Name: Holo", defaultValue: "Holo"))). Continue to loop more to unlock new icons! + """ + ) + ) + ) + static let rosePine = Icon( + name: .init(localized: .init("Icon Name: Rosé Pine", defaultValue: "Rosé Pine")), + iconName: "AppIcon-Rose Pine", + unlockTime: 50 + ) + static let metaLoop = Icon( + name: .init(localized: .init("Icon Name: Meta Loop", defaultValue: "Meta Loop")), + iconName: "AppIcon-Meta Loop", + unlockTime: 100 + ) + static let keycap = Icon( + name: .init(localized: .init("Icon Name: Keycap", defaultValue: "Keycap")), + iconName: "AppIcon-Keycap", + unlockTime: 200 + ) + static let white = Icon( + name: .init(localized: .init("Icon Name: White", defaultValue: "White")), + iconName: "AppIcon-White", + unlockTime: 400 + ) + static let black = Icon( + name: .init(localized: .init("Icon Name: Black", defaultValue: "Black")), + iconName: "AppIcon-Black", + unlockTime: 500 + ) + static let master = Icon( + name: .init(localized: .init("Icon Name: Loop Master", defaultValue: "Loop Master")), + iconName: "AppIcon-Loop Master", + unlockTime: 5000, + unlockMessage: .init( + localized: .init( + "Icon Unlock Message: Loop Master", + defaultValue: "5000 loops conquered! The universe has witnessed the birth of a Loop master! Enjoy your well-deserved reward: a brand-new icon!" + ) + ) + ) +} + +// MARK: - Greg Lassale + +extension Icon { + static let simon = Icon( + name: .init(localized: .init("Icon Name: Simon", defaultValue: "Simon")), + iconName: "AppIcon-Simon", + unlockTime: 1000 + ) + static let neon = Icon( + name: .init(localized: .init("Icon Name: Neon", defaultValue: "Neon")), + iconName: "AppIcon-Neon", + unlockTime: 1500 + ) + static let synthwaveSunset = Icon( + name: .init(localized: .init("Icon Name: Synthwave Sunset", defaultValue: "Synthwave Sunset")), + iconName: "AppIcon-Synthwave Sunset", + unlockTime: 2000 + ) + static let blackHole = Icon( + name: .init(localized: .init("Icon Name: Black Hole", defaultValue: "Black Hole")), + iconName: "AppIcon-Black Hole", + unlockTime: 2500 + ) +} + +// MARK: - JSDev + +extension Icon { + static let developer = Icon( + name: .init(localized: .init("Icon Name: Developer", defaultValue: "Developer")), + iconName: "AppIcon-Developer", + unlockTime: 0 + ) + + static let summer = Icon( + name: .init(localized: .init("Icon Name: Summer", defaultValue: "Summer")), + iconName: "AppIcon-Summer", + unlockTime: 3000 + ) +} diff --git a/Loop/Utilities/PaddingModel.swift b/Loop/Utilities/PaddingModel.swift index 1ac67b1d..645e8217 100644 --- a/Loop/Utilities/PaddingModel.swift +++ b/Loop/Utilities/PaddingModel.swift @@ -5,10 +5,10 @@ // Created by Kai Azim on 2024-02-01. // -import SwiftUI import Defaults +import SwiftUI -struct PaddingModel: Codable, Defaults.Serializable { +struct PaddingModel: Codable, Defaults.Serializable, Hashable { var window: CGFloat var externalBar: CGFloat var top: CGFloat @@ -19,7 +19,7 @@ struct PaddingModel: Codable, Defaults.Serializable { var configureScreenPadding: Bool var totalTopPadding: CGFloat { - self.top + externalBar + top + externalBar } static var zero = PaddingModel( @@ -33,10 +33,10 @@ struct PaddingModel: Codable, Defaults.Serializable { ) var totalVerticalPadding: CGFloat { - self.totalTopPadding + self.bottom + totalTopPadding + bottom } var totalHorizontalPadding: CGFloat { - self.right + self.left + right + left } } diff --git a/Loop/Utilities/ShakeEffect.swift b/Loop/Utilities/ShakeEffect.swift index 09b349a2..7239f61c 100644 --- a/Loop/Utilities/ShakeEffect.swift +++ b/Loop/Utilities/ShakeEffect.swift @@ -8,8 +8,8 @@ import SwiftUI struct ShakeEffect: GeometryEffect { - func effectValue(size: CGSize) -> ProjectionTransform { - return ProjectionTransform( + func effectValue(size _: CGSize) -> ProjectionTransform { + ProjectionTransform( CGAffineTransform( translationX: 3 * sin(position * 3 * .pi), y: 0 @@ -18,7 +18,7 @@ struct ShakeEffect: GeometryEffect { } init(shakes: Int) { - position = CGFloat(shakes) + self.position = CGFloat(shakes) } var position: CGFloat diff --git a/Loop/Utilities/SoftwareUpdater.swift b/Loop/Utilities/SoftwareUpdater.swift index 98d28108..bd36408d 100644 --- a/Loop/Utilities/SoftwareUpdater.swift +++ b/Loop/Utilities/SoftwareUpdater.swift @@ -5,9 +5,9 @@ // Created by Kai Azim on 2023-08-10. // +import Defaults import Foundation import Sparkle -import Defaults class SoftwareUpdater: NSObject, ObservableObject, SPUUpdaterDelegate { private var updater: SPUUpdater? @@ -26,13 +26,13 @@ class SoftwareUpdater: NSObject, ObservableObject, SPUUpdaterDelegate { override init() { super.init() - updater = SPUStandardUpdaterController( + self.updater = SPUStandardUpdaterController( startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil ).updater - automaticallyChecksForUpdatesObservation = updater?.observe( + self.automaticallyChecksForUpdatesObservation = updater?.observe( \.automaticallyChecksForUpdates, options: [.initial, .new, .old] ) { [weak self] updater, change in @@ -40,7 +40,7 @@ class SoftwareUpdater: NSObject, ObservableObject, SPUUpdaterDelegate { self?.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates } - lastUpdateCheckDateObservation = updater?.observe( + self.lastUpdateCheckDateObservation = updater?.observe( \.lastUpdateCheckDate, options: [.initial, .new, .old] ) { [weak self] updater, _ in @@ -52,7 +52,7 @@ class SoftwareUpdater: NSObject, ObservableObject, SPUUpdaterDelegate { feedURLTask?.cancel() } - func allowedChannels(for updater: SPUUpdater) -> Set { + func allowedChannels(for _: SPUUpdater) -> Set { Defaults[.includeDevelopmentVersions] ? ["development"] : [] } diff --git a/Loop/Utilities/UnstableIndicator.swift b/Loop/Utilities/UnstableIndicator.swift deleted file mode 100644 index fe454d8e..00000000 --- a/Loop/Utilities/UnstableIndicator.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// UnstableIndicator.swift -// Loop -// -// Created by Kai Azim on 2023-09-02. -// - -import SwiftUI - -struct UnstableIndicator: View { - let text: String - let color: Color - - init(_ text: String, color: Color) { - self.text = text - self.color = color - } - - var body: some View { - Text(text) - .font(.caption2) - .padding(.horizontal, 4) - .padding(.vertical, 1) - .background { - RoundedRectangle(cornerRadius: 50) - .stroke(lineWidth: 1) - } - .foregroundStyle(color) - } -} diff --git a/Loop/Utilities/VisualEffectView.swift b/Loop/Utilities/VisualEffectView.swift index b85dead5..e542a4e1 100644 --- a/Loop/Utilities/VisualEffectView.swift +++ b/Loop/Utilities/VisualEffectView.swift @@ -12,7 +12,7 @@ struct VisualEffectView: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode - func makeNSView(context: Context) -> NSVisualEffectView { + func makeNSView(context _: Context) -> NSVisualEffectView { let visualEffectView = NSVisualEffectView() visualEffectView.material = material visualEffectView.blendingMode = blendingMode @@ -21,7 +21,7 @@ struct VisualEffectView: NSViewRepresentable { return visualEffectView } - func updateNSView(_ visualEffectView: NSVisualEffectView, context: Context) { + func updateNSView(_ visualEffectView: NSVisualEffectView, context _: Context) { visualEffectView.material = material visualEffectView.blendingMode = blendingMode } diff --git a/Loop/Utilities/WallpaperView.swift b/Loop/Utilities/WallpaperView.swift index 2b68d545..20b261d0 100644 --- a/Loop/Utilities/WallpaperView.swift +++ b/Loop/Utilities/WallpaperView.swift @@ -10,7 +10,7 @@ import SwiftUI // By making this equatable, we won't refresh the view everytime something else changes // Make sure to apply .equatable() struct WallpaperView: View, Equatable { - static func == (lhs: WallpaperView, rhs: WallpaperView) -> Bool { + static func == (_: WallpaperView, _: WallpaperView) -> Bool { true } diff --git a/Loop/Window Management/Custom Window Sizes/CustomWindowActionAnchor.swift b/Loop/Window Management/Custom Window Sizes/CustomWindowActionAnchor.swift index 6c4ace8a..dea7faff 100644 --- a/Loop/Window Management/Custom Window Sizes/CustomWindowActionAnchor.swift +++ b/Loop/Window Management/Custom Window Sizes/CustomWindowActionAnchor.swift @@ -20,4 +20,19 @@ enum CustomWindowActionAnchor: Int, Codable, CaseIterable, Identifiable { case left = 7 case center = 8 case macOSCenter = 9 + + var iconAction: WindowAction { + switch self { + case .topLeft: .init(.topLeftQuarter) + case .top: .init(.topHalf) + case .topRight: .init(.topRightQuarter) + case .right: .init(.rightHalf) + case .bottomRight: .init(.bottomRightQuarter) + case .bottom: .init(.bottomHalf) + case .bottomLeft: .init(.bottomLeftQuarter) + case .left: .init(.leftHalf) + case .center: .init(.center) + case .macOSCenter: .init(.macOSCenter) + } + } } diff --git a/Loop/Window Management/Custom Window Sizes/CustomWindowActionPositionMode.swift b/Loop/Window Management/Custom Window Sizes/CustomWindowActionPositionMode.swift index f17d369f..a154e1c5 100644 --- a/Loop/Window Management/Custom Window Sizes/CustomWindowActionPositionMode.swift +++ b/Loop/Window Management/Custom Window Sizes/CustomWindowActionPositionMode.swift @@ -12,13 +12,4 @@ enum CustomWindowActionPositionMode: Int, Codable, CaseIterable, Identifiable { case generic = 0 case coordinates = 1 - - var label: Text { - switch self { - case .generic: - Text("\(Image(systemName: "rectangle.dashed")) Generic") - case .coordinates: - Text("\(Image("custom.scope.rectangle")) Coordinates") - } - } } diff --git a/Loop/Window Management/Custom Window Sizes/CustomWindowActionSizeMode.swift b/Loop/Window Management/Custom Window Sizes/CustomWindowActionSizeMode.swift index 3666b084..9ee7718e 100644 --- a/Loop/Window Management/Custom Window Sizes/CustomWindowActionSizeMode.swift +++ b/Loop/Window Management/Custom Window Sizes/CustomWindowActionSizeMode.swift @@ -14,14 +14,25 @@ enum CustomWindowActionSizeMode: Int, Codable, CaseIterable, Identifiable { case preserveSize = 1 case initialSize = 2 - var label: Text { + var name: String { switch self { case .custom: - Text("\(Image(systemName: "rectangle.dashed")) Custom") + "Custom" case .preserveSize: - Text("\(Image(systemName: "lock.rectangle")) Preserve Size") + "Preserve Size" case .initialSize: - Text("\( Image("custom.backward.end.alt.fill.2.rectangle")) Initial Size") + "Initial Size" + } + } + + var image: Image { + switch self { + case .custom: + Image(._18PxRulerPen) + case .preserveSize: + Image(._18PxTableLock) + case .initialSize: + Image(._18PxReturnKey) } } } diff --git a/Loop/Window Management/Custom Window Sizes/CustomWindowActionUnit.swift b/Loop/Window Management/Custom Window Sizes/CustomWindowActionUnit.swift index e39d4011..8503056e 100644 --- a/Loop/Window Management/Custom Window Sizes/CustomWindowActionUnit.swift +++ b/Loop/Window Management/Custom Window Sizes/CustomWindowActionUnit.swift @@ -13,16 +13,7 @@ enum CustomWindowActionUnit: Int, Codable, CaseIterable, Identifiable { case pixels = 0 case percentage = 1 - var label: Text { - switch self { - case .pixels: - Text("\(Image(systemName: "rectangle.checkered")) Pixels") - case .percentage: - Text("\(Image(systemName: "percent")) Percentages") - } - } - - var postscript: String { + var suffix: LocalizedStringKey { switch self { case .pixels: "px" diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index 83cebe15..41bd907a 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -5,8 +5,8 @@ // Created by Kai Azim on 2023-09-01. // -import SwiftUI import Defaults +import SwiftUI @_silgen_name("_AXUIElementGetWindow") @discardableResult func _AXUIElementGetWindow(_ axUiElement: AXUIElement, _ wid: inout CGWindowID) -> AXError @@ -49,9 +49,7 @@ class Window { convenience init?(pid: pid_t) { let element = AXUIElementCreateApplication(pid) guard let window = element.getValue(.focusedWindow) else { return nil } - // swiftlint:disable force_cast self.init(element: window as! AXUIElement) - // swiftlint:enable force_cast } func getPid() -> pid_t? { @@ -72,7 +70,7 @@ class Window { } var title: String? { - return self.axWindow.getValue(.title) as? String + self.axWindow.getValue(.title) as? String } var enhancedUserInterface: Bool? { @@ -83,7 +81,7 @@ class Window { } set { guard - let newValue = newValue, + let newValue, let pid = self.getPid() else { return @@ -102,8 +100,8 @@ class Window { var isAppExcluded: Bool { if let nsRunningApplication, - let bundleId = nsRunningApplication.bundleIdentifier { - return Defaults[.applicationExcludeList].contains(bundleId) + let path = nsRunningApplication.bundleURL { + return Defaults[.excludedApps].contains(path) } return false } @@ -112,10 +110,12 @@ class Window { let result = self.axWindow.getValue(.fullScreen) as? NSNumber return result?.boolValue ?? false } + @discardableResult func setFullscreen(_ state: Bool) -> Bool { - return self.axWindow.setValue(.fullScreen, value: state) + self.axWindow.setValue(.fullScreen, value: state) } + @discardableResult func toggleFullscreen() -> Bool { if !self.isFullscreen { @@ -125,8 +125,9 @@ class Window { } var isHidden: Bool { - return self.nsRunningApplication?.isHidden ?? false + self.nsRunningApplication?.isHidden ?? false } + @discardableResult func setHidden(_ state: Bool) -> Bool { var result = false @@ -137,6 +138,7 @@ class Window { } return result } + @discardableResult func toggleHidden() -> Bool { if !self.isHidden { @@ -149,10 +151,12 @@ class Window { let result = self.axWindow.getValue(.minimized) as? NSNumber return result?.boolValue ?? false } + @discardableResult func setMinimized(_ state: Bool) -> Bool { - return self.axWindow.setValue(.minimized, value: state) + self.axWindow.setValue(.minimized, value: state) } + @discardableResult func toggleMinimized() -> Bool { if !self.isMinimized { @@ -164,31 +168,29 @@ class Window { var position: CGPoint { var point: CGPoint = .zero guard let value = self.axWindow.getValue(.position) else { return point } - // swiftlint:disable force_cast - AXValueGetValue(value as! AXValue, .cgPoint, &point) // Convert to CGPoint - // swiftlint:enable force_cast + AXValueGetValue(value as! AXValue, .cgPoint, &point) // Convert to CGPoint return point } + @discardableResult func setPosition(_ position: CGPoint) -> Bool { - return self.axWindow.setValue(.position, value: position) + self.axWindow.setValue(.position, value: position) } var size: CGSize { var size: CGSize = .zero guard let value = self.axWindow.getValue(.size) else { return size } - // swiftlint:disable force_cast - AXValueGetValue(value as! AXValue, .cgSize, &size) // Convert to CGSize - // swiftlint:enable force_cast + AXValueGetValue(value as! AXValue, .cgSize, &size) // Convert to CGSize return size } + @discardableResult func setSize(_ size: CGSize) -> Bool { - return self.axWindow.setValue(.size, value: size) + self.axWindow.setValue(.size, value: size) } var frame: CGRect { - return CGRect(origin: self.position, size: self.size) + CGRect(origin: self.position, size: self.size) } func setFrame( @@ -196,7 +198,7 @@ class Window { animate: Bool = false, sizeFirst: Bool = false, // Only does something when window animations are off bounds: CGRect = .zero, // Only does something when window animations are on - completionHandler: @escaping (() -> Void) = {} + completionHandler: @escaping (() -> ()) = {} ) { let enhancedUI = self.enhancedUserInterface ?? false diff --git a/Loop/Window Management/WindowAction+Image.swift b/Loop/Window Management/WindowAction+Image.swift new file mode 100644 index 00000000..f6ec5efc --- /dev/null +++ b/Loop/Window Management/WindowAction+Image.swift @@ -0,0 +1,118 @@ +// +// WindowAction+Image.swift +// Loop +// +// Created by phlpsong on 2024/3/30. +// + +import SwiftUI + +extension WindowAction { + var icon: Image? { + switch direction { + case .noAction: + Image(systemName: "questionMark") + case .undo: + Image(systemName: "arrow.uturn.backward") + case .initialFrame: + Image(systemName: "backward.end.alt.fill") + case .hide: + Image(systemName: "eye.slash.fill") + case .minimize: + Image(systemName: "arrow.down.right.and.arrow.up.left") + case .nextScreen: + Image(systemName: "forward.fill") + case .previousScreen: + Image(systemName: "backward.fill") + case .larger: + Image(systemName: "arrow.up.left.and.arrow.down.right") + case .smaller: + Image(systemName: "arrow.down.right.and.arrow.up.left") + case .shrinkTop: + Image(systemName: "arrow.down") + case .shrinkBottom: + Image(systemName: "arrow.up") + case .shrinkRight: + Image(systemName: "arrow.left") + case .shrinkLeft: + Image(systemName: "arrow.right") + case .growTop: + Image(systemName: "arrow.up") + case .growBottom: + Image(systemName: "arrow.down") + case .growRight: + Image(systemName: "arrow.right") + case .growLeft: + Image(systemName: "arrow.left") + default: + nil + } + } + + var radialMenuImage: Image? { + switch direction { + case .hide: + Image("custom.rectangle.slash") + case .minimize: + Image("custom.arrow.down.right.and.arrow.up.left.rectangle") + default: + nil + } + } +} + +struct IconView: View { + @Binding var action: WindowAction + @State var frame: CGRect = .init(x: 0, y: 0, width: 1, height: 1) + + let size = CGSize(width: 14, height: 10) + let inset: CGFloat = 2 + let outerCornerRadius: CGFloat = 3 + + var body: some View { + if action.direction == .cycle, let first = action.cycle?.first { + IconView(action: .constant(first)) + } else { + ZStack { + if let icon = action.icon { + icon + .font(.system(size: 8)) + .fontWeight(.bold) + .frame(width: size.width, height: size.height) + } else { + ZStack { + RoundedRectangle(cornerRadius: outerCornerRadius - inset) + .frame( + width: frame.width, + height: frame.height + ) + .offset( + x: frame.origin.x, + y: frame.origin.y + ) + } + .frame(width: size.width, height: size.height, alignment: .topLeading) + .onAppear { + refreshFrame() + } + .onChange(of: action) { _ in + withAnimation(.easeOut(duration: 0.1)) { + refreshFrame() + } + } + } + } + .clipShape(.rect(cornerRadius: outerCornerRadius - inset)) + .background { + RoundedRectangle(cornerRadius: outerCornerRadius) + .stroke(lineWidth: 1.5) + .padding(-inset) + } + .padding(.horizontal, 4) + } + } + + func refreshFrame() { + frame = action.getFrame(window: nil, bounds: .init(origin: .zero, size: size), isPreview: true) + } +} diff --git a/Loop/Window Management/WindowAction+Port.swift b/Loop/Window Management/WindowAction+Port.swift index d729d475..ead70575 100644 --- a/Loop/Window Management/WindowAction+Port.swift +++ b/Loop/Window Management/WindowAction+Port.swift @@ -5,8 +5,8 @@ // Created by Kai Azim on 2024-03-22. // -import SwiftUI import Defaults +import SwiftUI extension WindowAction { private struct SavedWindowActionFormat: Codable { @@ -14,6 +14,7 @@ extension WindowAction { var keybind: Set // MARK: CUSTOM KEYBINDS + var name: String? var unit: CustomWindowActionUnit? var anchor: CustomWindowActionAnchor? @@ -27,7 +28,7 @@ extension WindowAction { var cycle: [SavedWindowActionFormat]? func convertToWindowAction() -> WindowAction { - return WindowAction( + WindowAction( direction, keybind: keybind, name: name, @@ -177,7 +178,7 @@ extension WindowAction { } } - private static func showAlertForImportDecision(completion: @escaping (ImportDecision) -> Void) { + private static func showAlertForImportDecision(completion: @escaping (ImportDecision) -> ()) { let alert = NSAlert() alert.messageText = "Import Keybinds" alert.informativeText = "Do you want to merge or erase existing keybinds?" @@ -188,7 +189,7 @@ extension WindowAction { alert.beginSheetModal(for: NSApplication.shared.mainWindow!) { response in switch response { - case .alertFirstButtonReturn: // Merge + case .alertFirstButtonReturn: // Merge completion(.merge) case .alertSecondButtonReturn: // Erase completion(.erase) diff --git a/Loop/Window Management/WindowAction.swift b/Loop/Window Management/WindowAction.swift index 96988a99..32be5b14 100644 --- a/Loop/Window Management/WindowAction.swift +++ b/Loop/Window Management/WindowAction.swift @@ -1,12 +1,12 @@ // -// Keybind.swift +// WindowAction.swift // Loop // // Created by Kai Azim on 2023-10-28. // -import SwiftUI import Defaults +import SwiftUI struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serializable { var id: UUID @@ -52,6 +52,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial var keybind: Set // MARK: CUSTOM KEYBINDS + var name: String? var unit: CustomWindowActionUnit? var anchor: CustomWindowActionAnchor? @@ -64,6 +65,28 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial var cycle: [WindowAction]? + func getName() -> String { + var result = "" + + if direction == .custom { + result = if let name, !name.isEmpty { + name + } else { + .init(localized: .init("Custom Keybind", defaultValue: "Custom Keybind")) + } + } else if direction == .cycle { + result = if let name, !name.isEmpty { + name + } else { + .init(localized: .init("Custom Cycle", defaultValue: "Custom Cycle")) + } + } else { + result = direction.name + } + + return result + } + var willManipulateCurrentWindowSize: Bool { direction.willAdjustSize || direction.willShrink || direction.willGrow } @@ -75,29 +98,6 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial return nil } - func getEdgesTouchingScreen() -> Edge.Set { - guard let frameMultiplyValues = direction.frameMultiplyValues else { - return [] - } - - var result: Edge.Set = [] - - if frameMultiplyValues.minX == 0 { - result.insert(.leading) - } - if frameMultiplyValues.maxX == 1 { - result.insert(.trailing) - } - if frameMultiplyValues.minY == 0 { - result.insert(.top) - } - if frameMultiplyValues.maxY == 1 { - result.insert(.bottom) - } - - return result - } - func radialMenuAngle(window: Window?) -> Angle? { guard direction.frameMultiplyValues != nil, @@ -107,19 +107,19 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } let frame = CGRect(origin: .zero, size: .init(width: 1, height: 1)) - let targetWindowFrame = getFrame(window: window, bounds: frame, toScale: false) + let targetWindowFrame = getFrame(window: window, bounds: frame, isPreview: true) let angle = frame.center.angle(to: targetWindowFrame.center) let result: Angle = .radians(angle) * -1 return result.normalized() } - func getFrame(window: Window?, bounds: CGRect, toScale: Bool = true) -> CGRect { - guard self.direction != .cycle, self.direction != .noAction else { + func getFrame(window: Window?, bounds: CGRect, isPreview: Bool = false) -> CGRect { + guard direction != .cycle, direction != .noAction else { return NSRect(origin: bounds.center, size: .zero) } var bounds = bounds - if toScale { bounds = getPaddedBounds(bounds) } + if !isPreview { bounds = getPaddedBounds(bounds) } var result = CGRect(origin: bounds.origin, size: .zero) if !willManipulateCurrentWindowSize { @@ -134,7 +134,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } else if direction.willAdjustSize { var frameToResizeFrom = LoopManager.lastTargetFrame - if !Defaults[.previewVisibility], let window = window { + if !Defaults[.previewVisibility], let window { frameToResizeFrom = window.frame } @@ -145,7 +145,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } else if direction.willShrink || direction.willGrow { var frameToResizeFrom = LoopManager.lastTargetFrame - if !Defaults[.previewVisibility], let window = window { + if !Defaults[.previewVisibility], let window { frameToResizeFrom = window.frame } @@ -168,8 +168,13 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } else if direction == .custom { result = calculateCustomFrame(window, bounds) - } else if direction == .center, let window = window { - let windowSize = window.size + } else if direction == .center { + let windowSize: CGSize = if let window { + window.size + } else { + .init(width: bounds.width / 2, height: bounds.height / 2) + } + result = CGRect( origin: CGPoint( x: bounds.midX - (windowSize.width / 2), @@ -178,8 +183,13 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial size: windowSize ) - } else if direction == .macOSCenter, let window = window { - let windowSize = window.size + } else if direction == .macOSCenter { + let windowSize: CGSize = if let window { + window.size + } else { + .init(width: bounds.width / 2, height: bounds.height / 2) + } + let yOffset = WindowEngine.getMacOSCenterYOffset( windowSize.height, screenHeight: bounds.height @@ -192,7 +202,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial ), size: windowSize ) - } else if direction == .undo, let window = window { + } else if direction == .undo, let window { if let previousAction = WindowRecords.getLastAction(for: window) { print("Last action was \(previousAction.direction) (name: \(previousAction.name ?? "nil"))") result = previousAction.getFrame(window: window, bounds: bounds) @@ -201,7 +211,7 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial result = window.frame } - } else if direction == .initialFrame, let window = window { + } else if direction == .initialFrame, let window { if let initialFrame = WindowRecords.getInitialFrame(for: window) { result = initialFrame } else { @@ -210,9 +220,9 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial } } - if toScale { - if direction != .undo && direction != .initialFrame { - result = self.applyPadding(result, bounds) + if !isPreview { + if direction != .undo, direction != .initialFrame { + result = applyPadding(result, bounds) } LoopManager.lastTargetFrame = result @@ -225,25 +235,31 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial var result = CGRect(origin: bounds.origin, size: .zero) // SIZE - if let sizeMode, sizeMode == .preserveSize, let window = window { + if let sizeMode, sizeMode == .preserveSize, let window { result.size = window.size - } else if let sizeMode, sizeMode == .initialSize, let window = window { + } else if let sizeMode, sizeMode == .initialSize, let window { if let initialFrame = WindowRecords.getInitialFrame(for: window) { result.size = initialFrame.size } - } else { // sizeMode would be custom + } else { // sizeMode would be custom switch unit { case .pixels: - result.size.width = width ?? result.size.width - result.size.height = height ?? result.size.height + if window == nil { + let mainScreen = NSScreen.main ?? NSScreen.screens[0] + result.size.width = (CGFloat(width ?? .zero) / mainScreen.frame.width) * bounds.width + result.size.height = (CGFloat(height ?? .zero) / mainScreen.frame.height) * bounds.height + } else { + result.size.width = width ?? .zero + result.size.height = height ?? .zero + } default: - if let width = width { + if let width { result.size.width = bounds.width * (width / 100.0) } - if let height = height { + if let height { result.size.height = bounds.height * (height / 100.0) } } @@ -253,19 +269,25 @@ struct WindowAction: Codable, Identifiable, Hashable, Equatable, Defaults.Serial if let positionMode, positionMode == .coordinates { switch unit { case .pixels: - // Note that bounds are ignored deliberately here - result.origin.x += xPoint ?? .zero - result.origin.y += yPoint ?? .zero + if window == nil { + let mainScreen = NSScreen.main ?? NSScreen.screens[0] + result.origin.x = (CGFloat(xPoint ?? .zero) / mainScreen.frame.width) * bounds.width + result.origin.y = (CGFloat(yPoint ?? .zero) / mainScreen.frame.height) * bounds.height + } else { + // Note that bounds are ignored deliberately here + result.origin.x += xPoint ?? .zero + result.origin.y += yPoint ?? .zero + } default: - if let xPoint = xPoint { + if let xPoint { result.origin.x += bounds.width * (xPoint / 100.0) } - if let yPoint = yPoint { + if let yPoint { result.origin.y += bounds.width * (yPoint / 100.0) } } - } else { // positionMode would be generic + } else { // positionMode would be generic switch anchor { case .top: result.origin.x = bounds.midX - result.width / 2 diff --git a/Loop/Window Management/WindowDirection+Image.swift b/Loop/Window Management/WindowDirection+Image.swift deleted file mode 100644 index cb8eab8a..00000000 --- a/Loop/Window Management/WindowDirection+Image.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// WindowDirection+Image.swift -// Loop -// -// Created by phlpsong on 2024/3/30. -// - -import SwiftUI - -extension WindowDirection { - var icon: Image? { - switch self { - case .maximize: - Image(systemName: "rectangle.inset.filled") - case .almostMaximize: - Image(systemName: "rectangle.center.inset.filled") - case .fullscreen: - Image(systemName: "rectangle.fill") - case .undo: - Image("custom.arrow.uturn.backward.rectangle") - case .initialFrame: - Image("custom.backward.end.alt.fill.2.rectangle") - case .hide: - Image("custom.rectangle.slash") - case .minimize: - Image("custom.arrow.down.right.and.arrow.up.left.rectangle") - case .center: - Image("custom.rectangle.center.inset.inset.filled") - case .macOSCenter: - Image("custom.rectangle.center.inset.inset.filled") - - case .topHalf: - Image(systemName: "rectangle.tophalf.inset.filled") - case .rightHalf: - Image(systemName: "rectangle.righthalf.inset.filled") - case .bottomHalf: - Image(systemName: "rectangle.bottomhalf.inset.filled") - case .leftHalf: - Image(systemName: "rectangle.lefthalf.inset.filled") - - case .topLeftQuarter: - Image(systemName: "rectangle.inset.topleft.filled") - case .topRightQuarter: - Image(systemName: "rectangle.inset.topright.filled") - case .bottomRightQuarter: - Image(systemName: "rectangle.inset.bottomright.filled") - case .bottomLeftQuarter: - Image(systemName: "rectangle.inset.bottomleft.filled") - - case .rightThird: - Image(systemName: "rectangle.rightthird.inset.filled") - case .rightTwoThirds: - Image("custom.rectangle.righttwothirds.inset.filled") - case .horizontalCenterThird: - Image("custom.rectangle.horizontalcenterthird.inset.filled") - case .leftThird: - Image(systemName: "rectangle.leftthird.inset.filled") - case .leftTwoThirds: - Image("custom.rectangle.lefttwothirds.inset.filled") - - case .topThird: - Image(systemName: "rectangle.topthird.inset.filled") - case .topTwoThirds: - Image("custom.rectangle.toptwothirds.inset.filled") - case .verticalCenterThird: - Image("custom.rectangle.verticalcenterthird.inset.filled") - case .bottomThird: - Image(systemName: "rectangle.bottomthird.inset.filled") - case .bottomTwoThirds: - Image("custom.rectangle.bottomtwothirds.inset.filled") - - case .nextScreen: - Image("custom.forward.rectangle") - case .previousScreen: - Image("custom.backward.rectangle") - - case .larger: - Image(systemName: "plus.rectangle") - case .smaller: - Image(systemName: "minus.rectangle") - - case .shrinkTop: - Image("custom.arrow.down.shrink.rectangle") - case .shrinkBottom: - Image("custom.arrow.up.shrink.rectangle") - case .shrinkRight: - Image("custom.arrow.left.shrink.rectangle") - case .shrinkLeft: - Image("custom.arrow.right.shrink.rectangle") - - case .growTop: - Image("custom.arrow.up.grow.rectangle") - case .growBottom: - Image("custom.arrow.down.grow.rectangle") - case .growRight: - Image("custom.arrow.right.grow.rectangle") - case .growLeft: - Image("custom.arrow.left.grow.rectangle") - - case .custom: - Image(systemName: "rectangle.dashed") - case .cycle: - Image("custom.arrow.2.squarepath.rectangle") - default: - nil - } - } - - var radialMenuImage: Image? { - switch self { - case .hide: - Image("custom.rectangle.slash") - case .minimize: - Image("custom.arrow.down.right.and.arrow.up.left.rectangle") - default: - nil - } - } -} diff --git a/Loop/Window Management/WindowDirection+LocalizedString.swift b/Loop/Window Management/WindowDirection+LocalizedString.swift index 5c4b851a..836e3421 100644 --- a/Loop/Window Management/WindowDirection+LocalizedString.swift +++ b/Loop/Window Management/WindowDirection+LocalizedString.swift @@ -6,15 +6,14 @@ // import Foundation +import Luminare extension WindowDirection { - var moreInformation: String? { - var result: String? + var infoView: LuminareInfoView? { + var result: LuminareInfoView? if self == .macOSCenter { - result = .init(localized: .init("Window Direction/More Information: macOS Center", defaultValue: """ -\(self.name) places windows slightly above the absolute center, which can be found more ergonomic. -""")) + result = .init("\(name) places windows slightly above the absolute center,\nwhich can be found more ergonomic.") } return result @@ -23,109 +22,109 @@ extension WindowDirection { var name: String { switch self { case .noAction: - .init(localized: .init("Window Direction/Name: No Action", defaultValue: "No Action")) + .init(localized: .init("Window Direction/Name: No Action", defaultValue: "No Action")) case .maximize: - .init(localized: .init("Window Direction/Name: Maximize", defaultValue: "Maximize")) + .init(localized: .init("Window Direction/Name: Maximize", defaultValue: "Maximize")) case .almostMaximize: - .init(localized: .init("Window Direction/Name: Almost Maximize", defaultValue: "Almost Maximize")) + .init(localized: .init("Window Direction/Name: Almost Maximize", defaultValue: "Almost Maximize")) case .fullscreen: - .init(localized: .init("Window Direction/Name: Fullscreen", defaultValue: "Fullscreen")) + .init(localized: .init("Window Direction/Name: Fullscreen", defaultValue: "Fullscreen")) case .undo: - .init(localized: .init("Window Direction/Name: Undo", defaultValue: "Undo")) + .init(localized: .init("Window Direction/Name: Undo", defaultValue: "Undo")) case .initialFrame: - .init(localized: .init("Window Direction/Name: Initial Frame", defaultValue: "Initial Frame")) + .init(localized: .init("Window Direction/Name: Initial Frame", defaultValue: "Initial Frame")) case .hide: - .init(localized: .init("Window Direction/Name: Hide", defaultValue: "Hide")) + .init(localized: .init("Window Direction/Name: Hide", defaultValue: "Hide")) case .minimize: - .init(localized: .init("Window Direction/Name: Minimize", defaultValue: "Minimize")) + .init(localized: .init("Window Direction/Name: Minimize", defaultValue: "Minimize")) case .macOSCenter: - .init(localized: .init("Window Direction/Name: macOS Center", defaultValue: "macOS Center")) + .init(localized: .init("Window Direction/Name: MacOS Center", defaultValue: "MacOS Center")) case .center: - .init(localized: .init("Window Direction/Name: Center", defaultValue: "Center")) + .init(localized: .init("Window Direction/Name: Center", defaultValue: "Center")) case .topHalf: - .init(localized: .init("Window Direction/Name: Top Half", defaultValue: "Top Half")) + .init(localized: .init("Window Direction/Name: Top Half", defaultValue: "Top Half")) case .rightHalf: - .init(localized: .init("Window Direction/Name: Right Half", defaultValue: "Right Half")) + .init(localized: .init("Window Direction/Name: Right Half", defaultValue: "Right Half")) case .bottomHalf: - .init(localized: .init("Window Direction/Name: Bottom Half", defaultValue: "Bottom Half")) + .init(localized: .init("Window Direction/Name: Bottom Half", defaultValue: "Bottom Half")) case .leftHalf: - .init(localized: .init("Window Direction/Name: Left Half", defaultValue: "Left Half")) + .init(localized: .init("Window Direction/Name: Left Half", defaultValue: "Left Half")) case .topLeftQuarter: - .init(localized: .init("Window Direction/Name: Top Left Quarter", defaultValue: "Top Left Quarter")) + .init(localized: .init("Window Direction/Name: Top Left Quarter", defaultValue: "Top Left Quarter")) case .topRightQuarter: - .init(localized: .init("Window Direction/Name: Top Right Quarter", defaultValue: "Top Right Quarter")) + .init(localized: .init("Window Direction/Name: Top Right Quarter", defaultValue: "Top Right Quarter")) case .bottomRightQuarter: - .init( - localized: .init( - "Window Direction/Name: Bottom Right Quarter", - defaultValue: "Bottom Right Quarter" - ) + .init( + localized: .init( + "Window Direction/Name: Bottom Right Quarter", + defaultValue: "Bottom Right Quarter" ) + ) case .bottomLeftQuarter: - .init( - localized: .init( - "Window Direction/Name: Bottom Left Quarter", - defaultValue: "Bottom Left Quarter" - ) + .init( + localized: .init( + "Window Direction/Name: Bottom Left Quarter", + defaultValue: "Bottom Left Quarter" ) + ) case .rightThird: - .init(localized: .init("Window Direction/Name: Right Third", defaultValue: "Right Third")) + .init(localized: .init("Window Direction/Name: Right Third", defaultValue: "Right Third")) case .rightTwoThirds: - .init(localized: .init("Window Direction/Name: Right Two Thirds", defaultValue: "Right Two Thirds")) + .init(localized: .init("Window Direction/Name: Right Two Thirds", defaultValue: "Right Two Thirds")) case .horizontalCenterThird: - .init( - localized: .init( - "Window Direction/Name: Horizontal Center Third", - defaultValue: "Horizontal Center Third" - ) + .init( + localized: .init( + "Window Direction/Name: Horizontal Center Third", + defaultValue: "Horizontal Center Third" ) + ) case .leftThird: - .init(localized: .init("Window Direction/Name: Left Third", defaultValue: "Left Third")) + .init(localized: .init("Window Direction/Name: Left Third", defaultValue: "Left Third")) case .leftTwoThirds: - .init(localized: .init("Window Direction/Name: Left Two Thirds", defaultValue: "Left Two Thirds")) + .init(localized: .init("Window Direction/Name: Left Two Thirds", defaultValue: "Left Two Thirds")) case .topThird: - .init(localized: .init("Window Direction/Name: Top Third", defaultValue: "Top Third")) + .init(localized: .init("Window Direction/Name: Top Third", defaultValue: "Top Third")) case .topTwoThirds: - .init(localized: .init("Window Direction/Name: Top Two Thirds", defaultValue: "Top Two Thirds")) + .init(localized: .init("Window Direction/Name: Top Two Thirds", defaultValue: "Top Two Thirds")) case .verticalCenterThird: - .init( - localized: .init( - "Window Direction/Name: Vertical Center Third", - defaultValue: "Vertical Center Third" - ) + .init( + localized: .init( + "Window Direction/Name: Vertical Center Third", + defaultValue: "Vertical Center Third" ) + ) case .bottomThird: - .init(localized: .init("Window Direction/Name: Bottom Third", defaultValue: "Bottom Third")) + .init(localized: .init("Window Direction/Name: Bottom Third", defaultValue: "Bottom Third")) case .bottomTwoThirds: - .init(localized: .init("Window Direction/Name: Bottom Two Thirds", defaultValue: "Bottom Two Thirds")) + .init(localized: .init("Window Direction/Name: Bottom Two Thirds", defaultValue: "Bottom Two Thirds")) case .nextScreen: - .init(localized: .init("Window Direction/Name: Next Screen", defaultValue: "Next Screen")) + .init(localized: .init("Window Direction/Name: Next Screen", defaultValue: "Next Screen")) case .previousScreen: - .init(localized: .init("Window Direction/Name: Previous Screen", defaultValue: "Previous Screen")) + .init(localized: .init("Window Direction/Name: Previous Screen", defaultValue: "Previous Screen")) case .larger: - .init(localized: .init("Window Direction/Name: Larger", defaultValue: "Larger")) + .init(localized: .init("Window Direction/Name: Larger", defaultValue: "Larger")) case .smaller: - .init(localized: .init("Window Direction/Name: Smaller", defaultValue: "Smaller")) + .init(localized: .init("Window Direction/Name: Smaller", defaultValue: "Smaller")) case .shrinkTop: - .init(localized: .init("Window Direction/Name: Shrink Top", defaultValue: "Shrink Top")) + .init(localized: .init("Window Direction/Name: Shrink Top", defaultValue: "Shrink Top")) case .shrinkBottom: - .init(localized: .init("Window Direction/Name: Shrink Bottom", defaultValue: "Shrink Bottom")) + .init(localized: .init("Window Direction/Name: Shrink Bottom", defaultValue: "Shrink Bottom")) case .shrinkRight: - .init(localized: .init("Window Direction/Name: Shrink Right", defaultValue: "Shrink Right")) + .init(localized: .init("Window Direction/Name: Shrink Right", defaultValue: "Shrink Right")) case .shrinkLeft: - .init(localized: .init("Window Direction/Name: Shrink Left", defaultValue: "Shrink Left")) + .init(localized: .init("Window Direction/Name: Shrink Left", defaultValue: "Shrink Left")) case .growTop: - .init(localized: .init("Window Direction/Name: Grow Top", defaultValue: "Grow Top")) + .init(localized: .init("Window Direction/Name: Grow Top", defaultValue: "Grow Top")) case .growBottom: - .init(localized: .init("Window Direction/Name: Grow Bottom", defaultValue: "Grow Bottom")) + .init(localized: .init("Window Direction/Name: Grow Bottom", defaultValue: "Grow Bottom")) case .growRight: - .init(localized: .init("Window Direction/Name: Grow Right", defaultValue: "Grow Right")) + .init(localized: .init("Window Direction/Name: Grow Right", defaultValue: "Grow Right")) case .growLeft: - .init(localized: .init("Window Direction/Name: Grow Left", defaultValue: "Grow Left")) + .init(localized: .init("Window Direction/Name: Grow Left", defaultValue: "Grow Left")) case .custom: - .init(localized: .init("Window Direction/Name: Custom", defaultValue: "Custom")) + .init(localized: .init("Window Direction/Name: Custom", defaultValue: "Custom")) case .cycle: - .init(localized: .init("Window Direction/Name: Cycle", defaultValue: "Cycle")) + .init(localized: .init("Window Direction/Name: Cycle", defaultValue: "Cycle")) } } } diff --git a/Loop/Window Management/WindowDirection+Snapping.swift b/Loop/Window Management/WindowDirection+Snapping.swift new file mode 100644 index 00000000..142311fe --- /dev/null +++ b/Loop/Window Management/WindowDirection+Snapping.swift @@ -0,0 +1,108 @@ +// +// WindowDirection+Snapping.swift +// Loop +// +// Created by Kai Azim on 2024-06-09. +// + +import Foundation + +extension WindowDirection { + static func processSnap( + mouseLocation: CGPoint, + currentDirection: WindowDirection, + screenFrame: CGRect, + ignoredFrame: CGRect + ) -> WindowDirection { + var newDirection: WindowDirection = .noAction + + if mouseLocation.x < ignoredFrame.minX { + newDirection = WindowDirection.processLeftSnap(mouseLocation, screenFrame) + } else if mouseLocation.x > ignoredFrame.maxX { + newDirection = WindowDirection.processRightSnap(mouseLocation, screenFrame) + } else if mouseLocation.y < ignoredFrame.minY { + newDirection = WindowDirection.processTopSnap(mouseLocation, screenFrame) + } else if mouseLocation.y > ignoredFrame.maxY { + newDirection = WindowDirection.processBottomSnap(mouseLocation, screenFrame, currentDirection) + } + + return newDirection + } + + static func processLeftSnap( + _ mouseLocation: CGPoint, + _ screenFrame: CGRect + ) -> WindowDirection { + let mouseY = mouseLocation.y + let maxY = screenFrame.maxY + let height = screenFrame.height + + if mouseY < maxY - (height * 7 / 8) { + return .topLeftQuarter + } + if mouseY > maxY - (height * 1 / 8) { + return .bottomLeftQuarter + } + return .leftHalf + } + + static func processRightSnap( + _ mouseLocation: CGPoint, + _ screenFrame: CGRect + ) -> WindowDirection { + let mouseY = mouseLocation.y + let maxY = screenFrame.maxY + let height = screenFrame.height + + if mouseY < maxY - (height * 7 / 8) { + return .topRightQuarter + } + if mouseY > maxY - (height * 1 / 8) { + return .bottomRightQuarter + } + return .rightHalf + } + + static func processTopSnap( + _ mouseLocation: CGPoint, + _ screenFrame: CGRect + ) -> WindowDirection { + let mouseX = mouseLocation.x + let maxX = screenFrame.maxX + let width = screenFrame.width + + if mouseX < maxX - (width * 4 / 5) || mouseX > maxX - (width * 1 / 5) { + return .topHalf + } + return .maximize + } + + static func processBottomSnap( + _ mouseLocation: CGPoint, + _ screenFrame: CGRect, + _ currentDirection: WindowDirection + ) -> WindowDirection { + var newDirection: WindowDirection + + let mouseX = mouseLocation.x + let maxX = screenFrame.maxX + let width = screenFrame.width + + if mouseX < maxX - (width * 2 / 3) { + newDirection = .leftThird + } else if mouseX > maxX - (width * 1 / 3) { + newDirection = .rightThird + } else { + // mouse is within 1/3 and 2/3 of the screen's width + newDirection = .bottomHalf + + if currentDirection == .leftThird || currentDirection == .leftTwoThirds { + newDirection = .leftTwoThirds + } else if currentDirection == .rightThird || currentDirection == .rightTwoThirds { + newDirection = .rightTwoThirds + } + } + + return newDirection + } +} diff --git a/Loop/Window Management/WindowDirection.swift b/Loop/Window Management/WindowDirection.swift index d216b9db..f9b9889d 100644 --- a/Loop/Window Management/WindowDirection.swift +++ b/Loop/Window Management/WindowDirection.swift @@ -5,11 +5,10 @@ // Created by Kai Azim on 2023-06-14. // -import SwiftUI import Defaults +import SwiftUI // Enum that stores all possible resizing options -// swiftlint:disable:next type_body_length enum WindowDirection: String, CaseIterable, Identifiable, Codable { var id: Self { self } @@ -79,30 +78,39 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { static var general: [WindowDirection] { [.fullscreen, .maximize, .almostMaximize, .center, .macOSCenter, .minimize, .hide] } + static var halves: [WindowDirection] { [.topHalf, .bottomHalf, .leftHalf, .rightHalf] } + static var quarters: [WindowDirection] { [.topLeftQuarter, .topRightQuarter, .bottomLeftQuarter, .bottomRightQuarter] } + static var horizontalThirds: [WindowDirection] { [.rightThird, .rightTwoThirds, .horizontalCenterThird, .leftTwoThirds, .leftThird] } + static var verticalThirds: [WindowDirection] { [.topThird, .topTwoThirds, .verticalCenterThird, .bottomTwoThirds, .bottomThird] } + static var screenSwitching: [WindowDirection] { [.nextScreen, .previousScreen] } + static var sizeAdjustment: [WindowDirection] { [.larger, .smaller] } + static var shrink: [WindowDirection] { [.shrinkTop, .shrinkBottom, .shrinkRight, .shrinkLeft] } + static var grow: [WindowDirection] { [.growTop, .growBottom, .growRight, .growLeft] } + static var more: [WindowDirection] { [.initialFrame, .undo, .custom, .cycle] } @@ -147,31 +155,6 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { } } - var radialMenuAngle: Double? { - switch self { - case .topHalf: - 0 - case .topRightQuarter: - 45 - case .rightHalf: - 90 - case .bottomRightQuarter: - 135 - case .bottomHalf: - 180 - case .bottomLeftQuarter: - 225 - case .leftHalf: - 270 - case .topLeftQuarter: - 315 - case .maximize: - 0 - default: - nil - } - } - var hasRadialMenuAngle: Bool { let noAngleActions: [WindowDirection] = [ .noAction, @@ -188,10 +171,10 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { ] if noAngleActions.contains(self) || - self.willChangeScreen || - self.willAdjustSize || - self.willShrink || - self.willGrow { + willChangeScreen || + willAdjustSize || + willShrink || + willGrow { return false } return true @@ -209,156 +192,58 @@ enum WindowDirection: String, CaseIterable, Identifiable, Codable { return fillActions.contains(self) } - static func processSnap( - mouseLocation: CGPoint, - currentDirection: WindowDirection, - screenFrame: CGRect, - ignoredFrame: CGRect - ) -> WindowDirection { - var newDirection: WindowDirection = .noAction - - if mouseLocation.x < ignoredFrame.minX { - newDirection = WindowDirection.processLeftSnap(mouseLocation, screenFrame) - } else if mouseLocation.x > ignoredFrame.maxX { - newDirection = WindowDirection.processRightSnap(mouseLocation, screenFrame) - } else if mouseLocation.y < ignoredFrame.minY { - newDirection = WindowDirection.processTopSnap(mouseLocation, screenFrame) - } else if mouseLocation.y > ignoredFrame.maxY { - newDirection = WindowDirection.processBottomSnap(mouseLocation, screenFrame, currentDirection) - } - - return newDirection - } - - static func processLeftSnap( - _ mouseLocation: CGPoint, - _ screenFrame: CGRect - ) -> WindowDirection { - let mouseY = mouseLocation.y - let maxY = screenFrame.maxY - let height = screenFrame.height - - if mouseY < maxY - (height * 7/8) { - return .topLeftQuarter - } - if mouseY > maxY - (height * 1/8) { - return .bottomLeftQuarter - } - return .leftHalf - } - - static func processRightSnap( - _ mouseLocation: CGPoint, - _ screenFrame: CGRect - ) -> WindowDirection { - let mouseY = mouseLocation.y - let maxY = screenFrame.maxY - let height = screenFrame.height - - if mouseY < maxY - (height * 7/8) { - return .topRightQuarter - } - if mouseY > maxY - (height * 1/8) { - return .bottomRightQuarter - } - return .rightHalf - } - - static func processTopSnap( - _ mouseLocation: CGPoint, - _ screenFrame: CGRect - ) -> WindowDirection { - let mouseX = mouseLocation.x - let maxX = screenFrame.maxX - let width = screenFrame.width - - if mouseX < maxX - (width * 4/5) || mouseX > maxX - (width * 1/5) { - return .topHalf - } - return .maximize - } - - static func processBottomSnap( - _ mouseLocation: CGPoint, - _ screenFrame: CGRect, - _ currentDirection: WindowDirection - ) -> WindowDirection { - var newDirection: WindowDirection - - let mouseX = mouseLocation.x - let maxX = screenFrame.maxX - let width = screenFrame.width - - if mouseX < maxX - (width * 2/3) { - newDirection = .leftThird - } else if mouseX > maxX - (width * 1/3) { - newDirection = .rightThird - } else { - // mouse is within 1/3 and 2/3 of the screen's width - newDirection = .bottomHalf - - if currentDirection == .leftThird || currentDirection == .leftTwoThirds { - newDirection = .leftTwoThirds - } else if currentDirection == .rightThird || currentDirection == .rightTwoThirds { - newDirection = .rightTwoThirds - } - } - - return newDirection - } - var frameMultiplyValues: CGRect? { switch self { case .maximize: CGRect(x: 0, y: 0, width: 1.0, height: 1.0) case .almostMaximize: - CGRect(x: 0.5/10.0, y: 0.5/10.0, width: 9.0/10.0, height: 9.0/10.0) + CGRect(x: 0.5 / 10.0, y: 0.5 / 10.0, width: 9.0 / 10.0, height: 9.0 / 10.0) case .fullscreen: CGRect(x: 0, y: 0, width: 1.0, height: 1.0) // Halves case .topHalf: - CGRect(x: 0, y: 0, width: 1.0, height: 1.0/2.0) + CGRect(x: 0, y: 0, width: 1.0, height: 1.0 / 2.0) case .rightHalf: - CGRect(x: 1.0/2.0, y: 0, width: 1.0/2.0, height: 1.0) + CGRect(x: 1.0 / 2.0, y: 0, width: 1.0 / 2.0, height: 1.0) case .bottomHalf: - CGRect(x: 0, y: 1.0/2.0, width: 1.0, height: 1.0/2.0) + CGRect(x: 0, y: 1.0 / 2.0, width: 1.0, height: 1.0 / 2.0) case .leftHalf: - CGRect(x: 0, y: 0, width: 1.0/2.0, height: 1.0) + CGRect(x: 0, y: 0, width: 1.0 / 2.0, height: 1.0) // Quarters case .topLeftQuarter: - CGRect(x: 0, y: 0, width: 1.0/2.0, height: 1.0/2.0) + CGRect(x: 0, y: 0, width: 1.0 / 2.0, height: 1.0 / 2.0) case .topRightQuarter: - CGRect(x: 1.0/2.0, y: 0, width: 1.0/2.0, height: 1.0/2.0) + CGRect(x: 1.0 / 2.0, y: 0, width: 1.0 / 2.0, height: 1.0 / 2.0) case .bottomRightQuarter: - CGRect(x: 1.0/2.0, y: 1.0/2.0, width: 1.0/2.0, height: 1.0/2.0) + CGRect(x: 1.0 / 2.0, y: 1.0 / 2.0, width: 1.0 / 2.0, height: 1.0 / 2.0) case .bottomLeftQuarter: - CGRect(x: 0, y: 1.0/2.0, width: 1.0/2.0, height: 1.0/2.0) + CGRect(x: 0, y: 1.0 / 2.0, width: 1.0 / 2.0, height: 1.0 / 2.0) // Thirds (Horizontal) case .rightThird: - CGRect(x: 2.0/3.0, y: 0, width: 1.0/3.0, height: 1.0) + CGRect(x: 2.0 / 3.0, y: 0, width: 1.0 / 3.0, height: 1.0) case .rightTwoThirds: - CGRect(x: 1.0/3.0, y: 0, width: 2.0/3.0, height: 1.0) + CGRect(x: 1.0 / 3.0, y: 0, width: 2.0 / 3.0, height: 1.0) case .horizontalCenterThird: - CGRect(x: 1.0/3.0, y: 0, width: 1.0/3.0, height: 1.0) + CGRect(x: 1.0 / 3.0, y: 0, width: 1.0 / 3.0, height: 1.0) case .leftThird: - CGRect(x: 0, y: 0, width: 1.0/3.0, height: 1.0) + CGRect(x: 0, y: 0, width: 1.0 / 3.0, height: 1.0) case .leftTwoThirds: - CGRect(x: 0, y: 0, width: 2.0/3.0, height: 1.0) + CGRect(x: 0, y: 0, width: 2.0 / 3.0, height: 1.0) // Thirds (Vertical) case .topThird: - CGRect(x: 0, y: 0, width: 1.0, height: 1.0/3.0) + CGRect(x: 0, y: 0, width: 1.0, height: 1.0 / 3.0) case .topTwoThirds: - CGRect(x: 0, y: 0, width: 1.0, height: 2.0/3.0) + CGRect(x: 0, y: 0, width: 1.0, height: 2.0 / 3.0) case .verticalCenterThird: - CGRect(x: 0, y: 1.0/3.0, width: 1.0, height: 1.0/3.0) + CGRect(x: 0, y: 1.0 / 3.0, width: 1.0, height: 1.0 / 3.0) case .bottomThird: - CGRect(x: 0, y: 2.0/3.0, width: 1.0, height: 1.0/3.0) + CGRect(x: 0, y: 2.0 / 3.0, width: 1.0, height: 1.0 / 3.0) case .bottomTwoThirds: - CGRect(x: 0, y: 1.0/3.0, width: 1.0, height: 2.0/3.0) + CGRect(x: 0, y: 1.0 / 3.0, width: 1.0, height: 2.0 / 3.0) default: nil } diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index 217420be..f9d41a8f 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -5,10 +5,10 @@ // Created by Kai Azim on 2023-06-16. // -import SwiftUI import Defaults +import SwiftUI -struct WindowEngine { +enum WindowEngine { /// Resize a Window /// - Parameters: /// - window: Window to be resized @@ -51,19 +51,41 @@ struct WindowEngine { return } - let targetWindowFrame = action.getFrame(window: window, bounds: screen.safeScreenFrame) + let targetFrame = action.getFrame(window: window, bounds: screen.safeScreenFrame) if action.direction == .undo { WindowRecords.removeLastAction(for: window) } - print("Target window frame: \(targetWindowFrame)") + print("Target window frame: \(targetFrame)") let enhancedUI = window.enhancedUserInterface ?? false let animate = Defaults[.animateWindowResizes] && !enhancedUI + WindowRecords.record(window, action) + + if window.nsRunningApplication == NSRunningApplication.current, + let window = NSApp.keyWindow { + var newFrame = targetFrame + newFrame.size = window.frame.size + + if newFrame.maxX > screen.safeScreenFrame.maxX { + newFrame.origin.x = screen.safeScreenFrame.maxX - newFrame.width - Defaults[.padding].right + } + + if newFrame.maxY > screen.safeScreenFrame.maxY { + newFrame.origin.y = screen.safeScreenFrame.maxY - newFrame.height - Defaults[.padding].bottom + } + + NSAnimationContext.runAnimationGroup { context in + context.timingFunction = CAMediaTimingFunction(controlPoints: 0.33, 1, 0.68, 1) + window.animator().setFrame(newFrame.flipY(screen: .screens[0]), display: false) + } + + return + } window.setFrame( - targetWindowFrame, + targetFrame, animate: animate, sizeFirst: willChangeScreens, bounds: screen.safeScreenFrame @@ -71,16 +93,18 @@ struct WindowEngine { // If animations are disabled, check if the window needs extra resizing if !animate { // Fixes an issue where window isn't resized correctly on multi-monitor setups - if !window.frame.approximatelyEqual(to: targetWindowFrame) { + if !window.frame.approximatelyEqual(to: targetFrame) { print("Backup resizing...") - window.setFrame(targetWindowFrame) + window.setFrame(targetFrame) } } WindowEngine.handleSizeConstrainedWindow(window: window, screenFrame: screen.safeScreenFrame) } - WindowRecords.record(window, action) + if Defaults[.moveCursorWithWindow] { + CGWarpMouseCursorPosition(targetFrame.center) + } } static func getTargetWindow() -> Window? { @@ -93,7 +117,7 @@ struct WindowEngine { } if result == nil { - result = WindowEngine.frontmostWindow + result = WindowEngine.frontmostWindow } return result @@ -114,7 +138,6 @@ struct WindowEngine { static func windowAtPosition(_ position: CGPoint) -> Window? { if let element = AXUIElement.systemWide.getElementAtPosition(position), let windowElement = element.getValue(.window), - // swiftlint:disable:next force_cast let window = Window(element: windowElement as! AXUIElement) { return window } @@ -159,7 +182,7 @@ struct WindowEngine { private static func handleSizeConstrainedWindow(window: Window, screenFrame: CGRect) { let windowFrame = window.frame // If the window is fully shown on the screen - if (windowFrame.maxX <= screenFrame.maxX) && (windowFrame.maxY <= screenFrame.maxY) { + if windowFrame.maxX <= screenFrame.maxX, windowFrame.maxY <= screenFrame.maxY { return } diff --git a/Loop/Window Management/WindowRecords.swift b/Loop/Window Management/WindowRecords.swift index 29cbf079..8a775c51 100644 --- a/Loop/Window Management/WindowRecords.swift +++ b/Loop/Window Management/WindowRecords.swift @@ -7,7 +7,7 @@ import SwiftUI -struct WindowRecords { +enum WindowRecords { private static var records: [WindowRecords.Record] = [] struct Record { @@ -20,7 +20,7 @@ struct WindowRecords { /// - Parameter window: The window to check /// - Returns: true or false static func hasBeenRecorded(_ window: Window) -> Bool { - return WindowRecords.records.contains { record in + WindowRecords.records.contains { record in record.cgWindowID == window.cgWindowID } } @@ -67,7 +67,7 @@ struct WindowRecords { /// - action: WindowAction to record static func record(_ window: Window, _ action: WindowAction) { guard - action.direction != .undo, // There is no point in recording undos + action.direction != .undo, // There is no point in recording undos let id = WindowRecords.findRecordsID(for: window) else { return diff --git a/Loop/Window Management/WindowTransformAnimation.swift b/Loop/Window Management/WindowTransformAnimation.swift index b387e9f2..ee2546ea 100644 --- a/Loop/Window Management/WindowTransformAnimation.swift +++ b/Loop/Window Management/WindowTransformAnimation.swift @@ -13,15 +13,15 @@ class WindowTransformAnimation: NSAnimation { private let originalFrame: CGRect private let window: Window private let bounds: CGRect - private let completionHandler: () -> Void + private let completionHandler: () -> () private var lastWindowFrame: CGRect = .zero // Using ids for each ongoing animation, we can cancel as a new window animation is started for that specific window - private var id: UUID = UUID() + private var id: UUID = .init() static var currentAnimations: [CGWindowID: UUID] = [:] - init(_ newRect: CGRect, window: Window, bounds: CGRect, completionHandler: @escaping () -> Void) { + init(_ newRect: CGRect, window: Window, bounds: CGRect, completionHandler: @escaping () -> ()) { self.targetFrame = newRect self.originalFrame = window.frame self.window = window @@ -32,16 +32,17 @@ class WindowTransformAnimation: NSAnimation { self.animationBlockingMode = .nonblocking self.lastWindowFrame = originalFrame - WindowTransformAnimation.currentAnimations[window.cgWindowID] = self.id + WindowTransformAnimation.currentAnimations[window.cgWindowID] = id } - required init?(coder: NSCoder) { + @available(*, unavailable) + required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } func startInBackground() { DispatchQueue.global().async { [self] in - self.start() + start() RunLoop.current.run() } } @@ -53,7 +54,7 @@ class WindowTransformAnimation: NSAnimation { return } - let value = CGFloat(1.0 - pow(1.0 - self.currentValue, 3)) + let value = CGFloat(1.0 - pow(1.0 - currentValue, 3)) var newFrame = CGRect( x: originalFrame.origin.x + value * (targetFrame.origin.x - originalFrame.origin.x),