diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 00000000..d1e187fb --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,9 @@ +excluded: + - Loop/Helpers/KeyCode.swift + +cyclomatic_complexity: + ignores_case_statements: true + +force_cast: warning + +line_length: 140 diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index c4c72746..72930b4a 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -48,6 +48,7 @@ /* Begin PBXFileReference section */ A82521EB29E234EB00139654 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; A82521ED29E235AC00139654 /* AccessibilityAccessManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityAccessManager.swift; sourceTree = ""; }; + A8291D6D2A4513D200C5CB69 /* .swiftlint.yml */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; tabWidth = 2; }; 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 = ""; }; A8330AC42A3AC15900673C8D /* Notification+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+Extensions.swift"; sourceTree = ""; }; @@ -191,6 +192,7 @@ A8E59C2C297F5E9A0064D4BA = { isa = PBXGroup; children = ( + A8291D6D2A4513D200C5CB69 /* .swiftlint.yml */, A8E6D1FC2A4155DC005751D4 /* .gitignore */, A86AFD7529888B29008F4892 /* README.md */, A8E59C37297F5E9A0064D4BA /* Loop */, @@ -240,6 +242,7 @@ isa = PBXNativeTarget; buildConfigurationList = A8E59C44297F5E9B0064D4BA /* Build configuration list for PBXNativeTarget "Loop" */; buildPhases = ( + A8291D6C2A450C2700C5CB69 /* Run SwiftLint */, A8E59C31297F5E9A0064D4BA /* Sources */, A8E59C32297F5E9A0064D4BA /* Frameworks */, A8E59C33297F5E9A0064D4BA /* Resources */, @@ -310,6 +313,25 @@ /* 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"; + }; A83F608129874F18005796CE /* Set Build Number */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; diff --git a/Loop/About Window/AboutView.swift b/Loop/About Window/AboutView.swift index e87c12cc..0a532652 100644 --- a/Loop/About Window/AboutView.swift +++ b/Loop/About Window/AboutView.swift @@ -7,34 +7,50 @@ import SwiftUI -struct packageDescription { +struct PackageDescription { var name: String var url: URL var license: URL } struct AboutView: View { - + @Environment(\.openURL) private var openURL - + @State private var isShowingAcknowledgements = false @State private var isHoveringOverIcon = false - - let PACKAGES: [packageDescription] = [ - packageDescription(name: "AppMover", url: URL(string: "https://github.com/iamcalledrob/AppMover")!, license: URL(string: "https://github.com/iamcalledrob/AppMover#license")!), - packageDescription(name: "Defaults", url: URL(string: "https://github.com/sindresorhus/Defaults")!, license: URL(string: "https://github.com/sindresorhus/Defaults/blob/main/license")!), - packageDescription(name: "Sparkle", url: URL(string: "https://sparkle-project.org")!, license: URL(string: "https://github.com/sparkle-project/Sparkle/blob/2.x/LICENSE")!), + + let packages: [PackageDescription] = [ + PackageDescription( + name: "AppMover", + url: URL(string: "https://github.com/iamcalledrob/AppMover")!, + license: URL(string: "https://github.com/iamcalledrob/AppMover#license")! + ), + PackageDescription( + name: "Defaults", + url: URL( + string: "https://github.com/sindresorhus/Defaults" + )!, + license: URL(string: "https://github.com/sindresorhus/Defaults/blob/main/license")! + ), + PackageDescription( + name: "Sparkle", + url: URL(string: "https://sparkle-project.org")!, + license: URL( + string: "https://github.com/sparkle-project/Sparkle/blob/2.x/LICENSE" + )! + ) ] - + var iconAnimation: Animation { Animation.snappy .speed(0.5) } - + var body: some View { VStack { VStack(spacing: 5) { - + // When user puts their cursor at the center of the icon, the icon will spin ZStack { Image(nsImage: NSApplication.shared.applicationIconImage) @@ -42,7 +58,7 @@ struct AboutView: View { .frame(width: 120, height: 120) .rotationEffect(Angle.degrees(isHoveringOverIcon ? 360 : 0)) .animation(iconAnimation, value: isHoveringOverIcon) - + // This is what the user needs to hover over Circle() .foregroundColor(.clear) @@ -51,24 +67,24 @@ struct AboutView: View { self.isHoveringOverIcon = hover } } - + Text("\(Bundle.main.appName)") .font(.title) .fontWeight(.bold) - + Text("Version \(Bundle.main.appVersion) (\(Bundle.main.appBuild))") .font(.caption2) .textSelection(.enabled) .foregroundColor(.secondary) } - + Spacer() - + Text("The elegant, mouse-oriented window manager") .multilineTextAlignment(.center) - + Spacer() - + Button { openURL(URL(string: "https://github.com/MrKai77/Loop")!) } label: { @@ -77,7 +93,7 @@ struct AboutView: View { .frame(maxWidth: .infinity) } .controlSize(.large) - + Button { self.isShowingAcknowledgements = true } label: { @@ -88,20 +104,20 @@ struct AboutView: View { .controlSize(.large) .popover(isPresented: $isShowingAcknowledgements) { VStack { - ForEach(0.. CFTypeRef? { - var ref: CFTypeRef? = nil + var ref: CFTypeRef? let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &ref) if error == .success { return ref } return .none } - + func getAttributeNames() -> [String]? { - var ref: CFArray? = nil + var ref: CFArray? let error = AXUIElementCopyAttributeNames(self, &ref) if error == .success { return ref! as [AnyObject] as? [String] diff --git a/Loop/Extensions/Bundle+Extensions.swift b/Loop/Extensions/Bundle+Extensions.swift index 3eff1ab9..3f8b928b 100644 --- a/Loop/Extensions/Bundle+Extensions.swift +++ b/Loop/Extensions/Bundle+Extensions.swift @@ -13,10 +13,10 @@ extension Bundle { var displayName: String { getInfo("CFBundleDisplayName") } var bundleID: String { getInfo("CFBundleIdentifier") } var copyright: String { getInfo("NSHumanReadableCopyright") } - + var appBuild: String { getInfo("CFBundleVersion") } var appVersion: String { getInfo("CFBundleShortVersionString") } - + func getInfo(_ str: String) -> String { infoDictionary?[str] as? String ?? "⚠️" } diff --git a/Loop/Extensions/CGPoint+Extensions.swift b/Loop/Extensions/CGPoint+Extensions.swift index ab6e7967..828374a6 100644 --- a/Loop/Extensions/CGPoint+Extensions.swift +++ b/Loop/Extensions/CGPoint+Extensions.swift @@ -17,11 +17,12 @@ extension CGPoint { return CGFloat(bearingRadians) } - + func distanceSquared(to comparisonPoint: CGPoint) -> CGFloat { let from = CGPoint(x: x, y: y) - let to = comparisonPoint - - return (from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y) + return (from.x - comparisonPoint.x) + * (from.x - comparisonPoint.x) + + (from.y - comparisonPoint.y) + * (from.y - comparisonPoint.y) } } diff --git a/Loop/Extensions/Defaults+Extensions.swift b/Loop/Extensions/Defaults+Extensions.swift index a9d1957a..8c5c9643 100644 --- a/Loop/Extensions/Defaults+Extensions.swift +++ b/Loop/Extensions/Defaults+Extensions.swift @@ -14,16 +14,16 @@ extension Defaults.Keys { static let currentIcon = Key("currentIcon", default: "AppIcon-Default") static let timesLooped = Key("timesLooped", default: 0) static let isAccessibilityAccessGranted = Key("isAccessibilityAccessGranted", default: false) - + static let useSystemAccentColor = Key("useSystemAccentColor", default: false) static let accentColor = Key("accentColor", default: Color(.white)) static let useGradientAccentColor = Key("useGradientAccentColor", default: false) static let gradientAccentColor = Key("gradientAccentColor", default: Color(.black)) - + static let triggerKey = Key("triggerKey", default: KeyCode.function) static let radialMenuCornerRadius = Key("radialMenuCornerRadius", default: 50) static let radialMenuThickness = Key("radialMenuThickness", default: 22) - + static let previewVisibility = Key("previewVisibility", default: true) static let previewCornerRadius = Key("previewCornerRadius", default: 15) static let previewPadding = Key("previewPadding", default: 10) diff --git a/Loop/Extensions/NSScreen+Extensions.swift b/Loop/Extensions/NSScreen+Extensions.swift index 6acb29a7..18c7d0be 100644 --- a/Loop/Extensions/NSScreen+Extensions.swift +++ b/Loop/Extensions/NSScreen+Extensions.swift @@ -10,11 +10,11 @@ import SwiftUI // Return the CGDirectDisplayID // Used in to help calculate the size a window needs to be resized to extension NSScreen { - var displayID: CGDirectDisplayID { + var displayID: CGDirectDisplayID? { let key = NSDeviceDescriptionKey("NSScreenNumber") - return self.deviceDescription[key] as! CGDirectDisplayID + return self.deviceDescription[key] as? CGDirectDisplayID } - + func screenWithMouse() -> NSScreen? { let mouseLocation = NSEvent.mouseLocation let screens = NSScreen.screens diff --git a/Loop/Helpers/AccessibilityAccessManager.swift b/Loop/Helpers/AccessibilityAccessManager.swift index 84e96fdd..6d99a0ba 100644 --- a/Loop/Helpers/AccessibilityAccessManager.swift +++ b/Loop/Helpers/AccessibilityAccessManager.swift @@ -14,17 +14,17 @@ class AccessibilityAccessManager { // Get current state for accessibility access let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeRetainedValue() as NSString: ask] let status = AXIsProcessTrustedWithOptions(options) - + Defaults[.isAccessibilityAccessGranted] = status return status } - + func accessibilityAccessAlert() { let alert = NSAlert() alert.messageText = "\(Bundle.main.appName) Needs Accessibility Permissions" alert.informativeText = "Welcome to \(Bundle.main.appName)! Please grant accessibility access to be able to resize windows." alert.runModal() - + checkAccessibilityAccess(ask: true) } } diff --git a/Loop/Helpers/IconManager.swift b/Loop/Helpers/IconManager.swift index 95e72a5a..357c04bd 100644 --- a/Loop/Helpers/IconManager.swift +++ b/Loop/Helpers/IconManager.swift @@ -9,19 +9,19 @@ import SwiftUI import Defaults class IconManager { - + // Icon name, times looped needed to unlock it let icons: [String: Int] = [ "AppIcon-Default": 0, "AppIcon-Scifi": 25, - "AppIcon-Rosé Pine": 50, + "AppIcon-Rosé Pine": 50 ] - + func nameWithoutPrefix(name: String) -> String { let prefix = "AppIcon-" return name.replacingOccurrences(of: prefix, with: "") } - + func returnUnlockedIcons() -> [String] { var returnValue: [String] = [] for (icon, unlockTimes) in icons where unlockTimes <= Defaults[.timesLooped] { @@ -29,26 +29,26 @@ class IconManager { } return returnValue.reversed() } - + func setAppIcon(to icon: String) { NSWorkspace.shared.setIcon(NSImage(named: icon), forFile: Bundle.main.bundlePath, options: []) NSApp.applicationIconImage = NSImage(named: icon) - + let alert = NSAlert() alert.messageText = "\(Bundle.main.appName)" alert.informativeText = "Current icon is now \(nameWithoutPrefix(name: icon))!" alert.icon = NSImage(named: icon) alert.runModal() - + Defaults[.currentIcon] = icon } - + // This function is run at startup to set the current icon to the user's set icon. func setCurrentAppIcon() { NSWorkspace.shared.setIcon(NSImage(named: Defaults[.currentIcon]), forFile: Bundle.main.bundlePath, options: []) NSApp.applicationIconImage = NSImage(named: Defaults[.currentIcon]) } - + func checkIfUnlockedNewIcon() { for (icon, unlockTimes) in icons where unlockTimes == Defaults[.timesLooped] { let alert = NSAlert() diff --git a/Loop/Helpers/KeyCode.swift b/Loop/Helpers/KeyCode.swift index 05955e62..50b63612 100644 --- a/Loop/Helpers/KeyCode.swift +++ b/Loop/Helpers/KeyCode.swift @@ -34,7 +34,7 @@ struct KeyCode { static let x = UInt16(kVK_ANSI_X) static let y = UInt16(kVK_ANSI_Y) static let z = UInt16(kVK_ANSI_Z) - + static let number0 = UInt16(kVK_ANSI_0) static let number1 = UInt16(kVK_ANSI_1) static let number2 = UInt16(kVK_ANSI_2) @@ -45,7 +45,7 @@ struct KeyCode { static let number7 = UInt16(kVK_ANSI_7) static let number8 = UInt16(kVK_ANSI_8) static let number9 = UInt16(kVK_ANSI_9) - + static let keypad0 = UInt16(kVK_ANSI_Keypad0) static let keypad1 = UInt16(kVK_ANSI_Keypad1) static let keypad2 = UInt16(kVK_ANSI_Keypad2) @@ -123,7 +123,7 @@ struct KeyCode { static let rightOption = UInt16(kVK_RightOption) static let shift = UInt16(kVK_Shift) static let rightShift = UInt16(kVK_RightShift) - + static let downArrow = UInt16(kVK_DownArrow) static let leftArrow = UInt16(kVK_LeftArrow) static let rightArrow = UInt16(kVK_RightArrow) diff --git a/Loop/Helpers/KeybindMonitor.swift b/Loop/Helpers/KeybindMonitor.swift index 0069f831..c92bbda9 100644 --- a/Loop/Helpers/KeybindMonitor.swift +++ b/Loop/Helpers/KeybindMonitor.swift @@ -8,22 +8,21 @@ import Cocoa class KeybindMonitor { - + static let shared = KeybindMonitor() - + private var eventTap: CFMachPort? private var isEnabled = false private var pressedKeys = Set() private var lastKeyReleaseTime: Date = Date.now - + func resetPressedKeys() { KeybindMonitor.shared.pressedKeys = [] } - + private func performKeybind(event: NSEvent) -> Bool { - var isValidKeybind = false - + // If the current key up event is within 100 ms of the last key up event, return. // This is used when the user is pressing 2+ keys so that it doesn't switch back // to the one key direction when they're letting go of the keys. @@ -33,7 +32,7 @@ class KeybindMonitor { } return false } - + if pressedKeys == [KeyCode.escape] { NotificationCenter.default.post( name: Notification.Name.closeLoop, @@ -42,34 +41,31 @@ class KeybindMonitor { ) KeybindMonitor.shared.resetPressedKeys() isValidKeybind = true - } - else { + } else { // Since this is one for loop inside another, we can break from inside by breaking from the outerloop outerLoop: for direction in WindowDirection.allCases { - for keybind in direction.keybindings { - if keybind == pressedKeys { - NotificationCenter.default.post( - name: Notification.Name.currentDirectionChanged, - object: nil, - userInfo: ["Direction": direction] - ) - isValidKeybind = true - break outerLoop - } + for keybind in direction.keybindings where keybind == pressedKeys { + NotificationCenter.default.post( + name: Notification.Name.currentDirectionChanged, + object: nil, + userInfo: ["Direction": direction] + ) + isValidKeybind = true + break outerLoop } } } return isValidKeybind } - + func start() { if eventTap == nil { let eventMask = CGEventMask((1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)) - + let eventCallback: CGEventTapCallBack = { _, _, event, _ in if KeybindMonitor.shared.isEnabled, let keyEvent = NSEvent(cgEvent: event) { - + if !keyEvent.isARepeat { if keyEvent.type == .keyUp { KeybindMonitor.shared.pressedKeys.remove(keyEvent.keyCode) @@ -77,12 +73,12 @@ class KeybindMonitor { KeybindMonitor.shared.pressedKeys.insert(keyEvent.keyCode) } } - + if KeybindMonitor.shared.performKeybind(event: keyEvent) { return nil } } - + // If we wanted to forward the key event to the frontmost app, we'd use: // return Unmanaged.passRetained(event) return nil @@ -103,7 +99,7 @@ class KeybindMonitor { } isEnabled = true } - + func stop() { if let eventTap = eventTap { CGEvent.tapEnable(tap: eventTap, enable: false) @@ -111,6 +107,5 @@ class KeybindMonitor { self.eventTap = nil } isEnabled = false - } } diff --git a/Loop/Helpers/LoopTriggerKeys.swift b/Loop/Helpers/LoopTriggerKeys.swift index 9759ed42..bbb07ca2 100644 --- a/Loop/Helpers/LoopTriggerKeys.swift +++ b/Loop/Helpers/LoopTriggerKeys.swift @@ -12,7 +12,7 @@ struct LoopTriggerKeys { var keySymbol: String var description: String var keycode: UInt16 - + static let options: [LoopTriggerKeys] = [ LoopTriggerKeys( symbol: "globe", @@ -37,6 +37,6 @@ struct LoopTriggerKeys { keySymbol: "custom.command.rectangle.fill", description: "Right Command", keycode: KeyCode.rightCommand - ), + ) ] } diff --git a/Loop/Helpers/VisualEffectView.swift b/Loop/Helpers/VisualEffectView.swift index 94ed3fcb..b85dead5 100644 --- a/Loop/Helpers/VisualEffectView.swift +++ b/Loop/Helpers/VisualEffectView.swift @@ -11,7 +11,7 @@ import SwiftUI struct VisualEffectView: NSViewRepresentable { let material: NSVisualEffectView.Material let blendingMode: NSVisualEffectView.BlendingMode - + func makeNSView(context: Context) -> NSVisualEffectView { let visualEffectView = NSVisualEffectView() visualEffectView.material = material @@ -26,4 +26,3 @@ struct VisualEffectView: NSViewRepresentable { visualEffectView.blendingMode = blendingMode } } - diff --git a/Loop/Helpers/WindowDirection.swift b/Loop/Helpers/WindowDirection.swift index d7741e25..4f6ce186 100644 --- a/Loop/Helpers/WindowDirection.swift +++ b/Loop/Helpers/WindowDirection.swift @@ -9,35 +9,35 @@ import SwiftUI // Enum that stores all possible resizing options enum WindowDirection: CaseIterable { - + case noAction case maximize - + // Halves case topHalf case rightHalf case bottomHalf case leftHalf - + // Quarters case topRightQuarter case bottomRightQuarter case bottomLeftQuarter case topLeftQuarter - + // The following aren't accessible from the radial menu case rightThird case rightTwoThirds case horizontalCenterThird case leftThird case leftTwoThirds - + case topThird case topTwoThirds case verticalCenterThird case bottomThird case bottomTwoThirds - + var nextWindowDirection: WindowDirection { switch self { case .noAction: .topHalf @@ -53,29 +53,29 @@ enum WindowDirection: CaseIterable { default: .noAction } } - + var keybindings: [Set] { switch self { - case .noAction: [[]] - case .maximize: [[KeyCode.space]] - - case .topHalf: [[KeyCode.w], [KeyCode.upArrow]] - case .rightHalf: [[KeyCode.d], [KeyCode.rightArrow]] - case .bottomHalf: [[KeyCode.s], [KeyCode.downArrow]] - case .leftHalf: [[KeyCode.a], [KeyCode.leftArrow]] - - case .topRightQuarter: [[KeyCode.w, KeyCode.d], [KeyCode.upArrow, KeyCode.rightArrow]] - case .bottomRightQuarter: [[KeyCode.s, KeyCode.d], [KeyCode.downArrow, KeyCode.rightArrow]] - case .bottomLeftQuarter: [[KeyCode.s, KeyCode.a], [KeyCode.downArrow, KeyCode.leftArrow]] - case .topLeftQuarter: [[KeyCode.w, KeyCode.a], [KeyCode.upArrow, KeyCode.leftArrow]] - - case .leftThird: [[KeyCode.j]] - case .leftTwoThirds: [[KeyCode.u]] - case .horizontalCenterThird:[[KeyCode.k]] - case .rightTwoThirds: [[KeyCode.o]] - case .rightThird: [[KeyCode.l]] - - default: [[]] + case .noAction: [[]] + case .maximize: [[KeyCode.space]] + + case .topHalf: [[KeyCode.w], [KeyCode.upArrow]] + case .rightHalf: [[KeyCode.d], [KeyCode.rightArrow]] + case .bottomHalf: [[KeyCode.s], [KeyCode.downArrow]] + case .leftHalf: [[KeyCode.a], [KeyCode.leftArrow]] + + case .topRightQuarter: [[KeyCode.w, KeyCode.d], [KeyCode.upArrow, KeyCode.rightArrow]] + case .bottomRightQuarter: [[KeyCode.s, KeyCode.d], [KeyCode.downArrow, KeyCode.rightArrow]] + case .bottomLeftQuarter: [[KeyCode.s, KeyCode.a], [KeyCode.downArrow, KeyCode.leftArrow]] + case .topLeftQuarter: [[KeyCode.w, KeyCode.a], [KeyCode.upArrow, KeyCode.leftArrow]] + + case .leftThird: [[KeyCode.j]] + case .leftTwoThirds: [[KeyCode.u]] + case .horizontalCenterThird: [[KeyCode.k]] + case .rightTwoThirds: [[KeyCode.o]] + case .rightThird: [[KeyCode.l]] + + default: [[]] } } } diff --git a/Loop/Helpers/WindowEngine.swift b/Loop/Helpers/WindowEngine.swift index ad092314..05b5cc66 100644 --- a/Loop/Helpers/WindowEngine.swift +++ b/Loop/Helpers/WindowEngine.swift @@ -7,15 +7,15 @@ import SwiftUI -fileprivate let kAXFullScreenAttribute = "AXFullScreen" - struct WindowEngine { - + + private let kAXFullScreenAttribute = "AXFullScreen" + func resizeFrontmostWindow(direction: WindowDirection) { guard let frontmostWindow = self.getFrontmostWindow() else { return } resize(window: frontmostWindow, direction: direction) } - + func getFrontmostWindow() -> AXUIElement? { guard let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive }), let window = self.getFocusedWindow(pid: app.processIdentifier), @@ -23,29 +23,35 @@ struct WindowEngine { self.getSubRole(element: window) == kAXStandardWindowSubrole, self.isFullscreen(element: window) == false else { return nil } - + return window } - + func resize(window: AXUIElement, direction: WindowDirection) { guard let screenFrame = getActiveScreenFrame(), let newWindowFrame = generateWindowFrame(screenFrame, direction) else { return } - + self.setPosition(element: window, position: newWindowFrame.origin) self.setSize(element: window, size: newWindowFrame.size) - + if self.getRect(element: window) != newWindowFrame { - self.handleSizeConstrainedWindow(element: window, windowFrame: self.getRect(element: window), screenFrame: screenFrame) + self.handleSizeConstrainedWindow( + element: window, + windowFrame: self.getRect(element: window), + screenFrame: screenFrame + ) } - + KeybindMonitor.shared.resetPressedKeys() } - + private func getFocusedWindow(pid: pid_t) -> AXUIElement? { let element = AXUIElementCreateApplication(pid) if let window = element.copyAttributeValue(attribute: kAXFocusedWindowAttribute) { + // swiftlint:disable force_cast return (window as! AXUIElement) + // swiftlint:enable force_cast } return nil } @@ -59,7 +65,7 @@ struct WindowEngine { let result = element.copyAttributeValue(attribute: kAXFullScreenAttribute) as? NSNumber return result?.boolValue ?? false } - + @discardableResult private func setPosition(element: AXUIElement, position: CGPoint) -> Bool { var position = position @@ -71,10 +77,12 @@ struct WindowEngine { private func getPosition(element: AXUIElement) -> CGPoint { var point: CGPoint = .zero guard let axValue = element.copyAttributeValue(attribute: kAXPositionAttribute) else { return point } + // swiftlint:disable force_cast AXValueGetValue(axValue as! AXValue, .cgPoint, &point) + // swiftlint:enable force_cast return point } - + @discardableResult private func setSize(element: AXUIElement, size: CGSize) -> Bool { var size = size @@ -86,30 +94,33 @@ struct WindowEngine { private func getSize(element: AXUIElement) -> CGSize { var size: CGSize = .zero guard let axValue = element.copyAttributeValue(attribute: kAXSizeAttribute) else { return size } + // swiftlint:disable force_cast AXValueGetValue(axValue as! AXValue, .cgSize, &size) + // swiftlint:enable force_cast return size } - + private func getRect(element: AXUIElement) -> CGRect { return CGRect(origin: getPosition(element: element), size: getSize(element: element)) } - + private func getActiveScreenFrame() -> CGRect? { guard let screen = NSScreen().screenWithMouse() else { return nil } + guard let displayID = screen.displayID else { return nil } let menubarHeight = screen.frame.size.height - screen.visibleFrame.size.height - var screenFrame = CGDisplayBounds(screen.displayID) + var screenFrame = CGDisplayBounds(displayID) screenFrame.size.height -= menubarHeight screenFrame.origin.y += menubarHeight - + return screenFrame } private func generateWindowFrame(_ screenFrame: CGRect, _ direction: WindowDirection) -> CGRect? { - + let screenWidth = screenFrame.size.width let screenHeight = screenFrame.size.height let screenX = screenFrame.origin.x let screenY = screenFrame.origin.y - + switch direction { case .topHalf: return CGRect(x: screenX, y: screenY, width: screenWidth, height: screenHeight/2) @@ -149,30 +160,30 @@ struct WindowEngine { return CGRect(x: screenX, y: screenY+2*screenHeight/3, width: screenWidth, height: screenHeight/3) case .bottomTwoThirds: return CGRect(x: screenX, y: screenY+screenHeight/3, width: screenWidth, height: 2*screenHeight/3) - + default: return nil } } - + private func handleSizeConstrainedWindow(element: AXUIElement, windowFrame: CGRect, screenFrame: CGRect) { - + // If the window is fully shown on the screen if (windowFrame.maxX <= screenFrame.maxX) && (windowFrame.maxY <= screenFrame.maxY) { return } - + // If not, then Loop will auto re-adjust the window size to be fully shown on the screen var fixedWindowFrame = windowFrame - + if fixedWindowFrame.maxX > screenFrame.maxX { fixedWindowFrame.origin.x = screenFrame.maxX - fixedWindowFrame.width } - + if fixedWindowFrame.maxY > screenFrame.maxY { fixedWindowFrame.origin.y = screenFrame.maxY - fixedWindowFrame.height } - + setPosition(element: element, position: fixedWindowFrame.origin) } } diff --git a/Loop/LoopApp.swift b/Loop/LoopApp.swift index 38501b49..20200453 100644 --- a/Loop/LoopApp.swift +++ b/Loop/LoopApp.swift @@ -12,10 +12,10 @@ import AppMover @main struct LoopApp: App { - + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate let aboutViewController = AboutViewController() - + var body: some Scene { Settings { SettingsView() @@ -30,23 +30,21 @@ struct LoopApp: App { .keyboardShortcut("q") } } - - + MenuBarExtra("Loop", image: "menubarIcon") { if #available(macOS 14, *) { SettingsLink() - } - else { + } else { Button("Settings") { NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) NSApp.activate(ignoringOtherApps: true) } } - + Button("About \(Bundle.main.appName)") { aboutViewController.showAboutWindow() } - + Button("Quit") { NSApp.terminate(nil) } @@ -55,29 +53,29 @@ struct LoopApp: App { } class AppDelegate: NSObject, NSApplicationDelegate { - + let windowEngine = WindowEngine() let radialMenuController = RadialMenuController() let aboutViewController = AboutViewController() let iconManager = IconManager() let accessibilityAccessManager = AccessibilityAccessManager() - + func applicationDidFinishLaunching(_ notification: Notification) { - do { try AppMover.moveApp() } catch { print("Moving app failed: \(error)") } - - // Check accessibility access, then if access is not granted, show a more informative alert asking for accessibility access + + // Check accessibility access, then if access is not granted, + // show a more informative alert asking for accessibility access if !accessibilityAccessManager.checkAccessibilityAccess(ask: false) { accessibilityAccessManager.accessibilityAccessAlert() } iconManager.setCurrentAppIcon() - - radialMenuController.AddObservers() - + + radialMenuController.addObservers() + // Show settings window on launch if this is a debug build #if DEBUG print("Debug build!") diff --git a/Loop/Menubar/LoopMenubarController.swift b/Loop/Menubar/LoopMenubarController.swift deleted file mode 100755 index 084ff779..00000000 --- a/Loop/Menubar/LoopMenubarController.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// LoopMenubarController.swift -// Loop -// -// Created by Kai Azim on 2023-01-24. -// - -import Cocoa -import KeyboardShortcuts - -struct resizeWindowMenuItem { - var divider: Bool? - var title: String? - var selector: Selector? - var shortcut: KeyboardShortcuts.Name? -} - -class LoopMenubarController { - - let resizeWindowMenuItems = [ - resizeWindowMenuItem(title: "Maximize", selector: #selector(resizeWindowMaximize), shortcut: .resizeMaximize), - resizeWindowMenuItem(divider: true), - resizeWindowMenuItem(title: "Top Half", selector: #selector(resizeWindowTopHalf), shortcut: .resizeTopHalf), - resizeWindowMenuItem(title: "Bottom Half", selector: #selector(resizeWindowBottomHalf), shortcut: .resizeBottomHalf), - resizeWindowMenuItem(title: "Right Half", selector: #selector(resizeWindowRightHalf), shortcut: .resizeRightHalf), - resizeWindowMenuItem(title: "Left Half", selector: #selector(resizeWindowLeftHalf), shortcut: .resizeLeftHalf), - resizeWindowMenuItem(divider: true), - resizeWindowMenuItem(title: "Top Right Quarter", selector: #selector(resizeWindowTopRightQuarter), shortcut: .resizeTopRightQuarter), - resizeWindowMenuItem(title: "Top Left Quarter", selector: #selector(resizeWindowTopLeftQuarter), shortcut: .resizeTopLeftQuarter), - resizeWindowMenuItem(title: "Bottom Right Quarter", selector: #selector(resizeWindowBottomRightQuarter), shortcut: .resizeBottomRightQuarter), - resizeWindowMenuItem(title: "Bottom Left Quarter", selector: #selector(resizeWindowBottomLeftQuarter), shortcut: .resizeBottomLeftQuarter), - resizeWindowMenuItem(divider: true), - resizeWindowMenuItem(title: "Right Third", selector: #selector(resizeWindowRightThird), shortcut: .resizeRightThird), - resizeWindowMenuItem(title: "Right Two Thirds", selector: #selector(resizeWindowRightTwoThirds), shortcut: .resizeRightTwoThirds), - resizeWindowMenuItem(title: "Center Third", selector: #selector(resizeWindowRLCenterThird), shortcut: .resizeRLCenterThird), - resizeWindowMenuItem(title: "Left Two Thirds", selector: #selector(resizeWindowLeftTwoThirds), shortcut: .resizeLeftTwoThirds), - resizeWindowMenuItem(title: "Left Third", selector: #selector(resizeWindowLeftThird), shortcut: .resizeLeftThird) - ] - - let windowResizer = WindowResizer() - private var statusItem: NSStatusItem! - - func show() { - statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) - guard let button = statusItem.button else { return } - button.image = NSImage(named: NSImage.Name("menubarIcon")) - - let loopMenu = NSMenu() - - let resizeWindow = NSMenuItem(title: "Resize Window", action: nil, keyEquivalent: "") - resizeWindow.submenu = NSMenu() - - for item in resizeWindowMenuItems { - if item.divider != nil { - resizeWindow.submenu?.addItem(NSMenuItem.separator()) - } else { - let menuItem = NSMenuItem(title: item.title!, action: item.selector, keyEquivalent: "") - menuItem.setShortcut(for: item.shortcut) - menuItem.target = self - - resizeWindow.submenu?.addItem(menuItem) - } - } - - loopMenu.addItem(resizeWindow) - loopMenu.addItem(NSMenuItem.separator()) - if #available(macOS 13, *) { - loopMenu.addItem(withTitle: "Settings", action: #selector(self.openSettings), keyEquivalent: ",").target = self - } else { - loopMenu.addItem(withTitle: "Preferences", action: #selector(self.openSettings), keyEquivalent: ",").target = self - } - loopMenu.addItem(withTitle: "Quit", action: #selector(NSApp.terminate(_:)), keyEquivalent: "q") - - statusItem.menu = loopMenu - } - - @objc func resizeWindowMaximize() { - self.windowResizer.resizeFrontmostWindowWithDirection(.maximize) - } - - @objc func resizeWindowTopHalf() { - self.windowResizer.resizeFrontmostWindowWithDirection(.topHalf) - } - @objc func resizeWindowBottomHalf() { - self.windowResizer.resizeFrontmostWindowWithDirection(.bottomHalf) - } - @objc func resizeWindowRightHalf() { - self.windowResizer.resizeFrontmostWindowWithDirection(.rightHalf) - } - @objc func resizeWindowLeftHalf() { - self.windowResizer.resizeFrontmostWindowWithDirection(.leftHalf) - } - - @objc func resizeWindowTopRightQuarter() { - self.windowResizer.resizeFrontmostWindowWithDirection(.topRightQuarter) - } - @objc func resizeWindowTopLeftQuarter() { - self.windowResizer.resizeFrontmostWindowWithDirection(.topLeftQuarter) - } - @objc func resizeWindowBottomRightQuarter() { - self.windowResizer.resizeFrontmostWindowWithDirection(.bottomRightQuarter) - } - @objc func resizeWindowBottomLeftQuarter() { - self.windowResizer.resizeFrontmostWindowWithDirection(.bottomLeftQuarter) - } - - @objc func resizeWindowRightThird() { - self.windowResizer.resizeFrontmostWindowWithDirection(.rightThird) - } - @objc func resizeWindowRightTwoThirds() { - self.windowResizer.resizeFrontmostWindowWithDirection(.rightTwoThirds) - } - @objc func resizeWindowRLCenterThird() { - self.windowResizer.resizeFrontmostWindowWithDirection(.RLcenterThird) - } - @objc func resizeWindowLeftTwoThirds() { - self.windowResizer.resizeFrontmostWindowWithDirection(.leftTwoThirds) - } - @objc func resizeWindowLeftThird() { - self.windowResizer.resizeFrontmostWindowWithDirection(.leftThird) - } - - @objc func openSettings() { - if #available(macOS 13, *) { - NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) - } else { - NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) - } - NSApp.activate(ignoringOtherApps: true) - } -} diff --git a/Loop/Preview Window/PreviewController.swift b/Loop/Preview Window/PreviewController.swift index c10726dc..625fa483 100644 --- a/Loop/Preview Window/PreviewController.swift +++ b/Loop/Preview Window/PreviewController.swift @@ -8,15 +8,15 @@ import SwiftUI class PreviewController { - + var loopPreviewWindowController: NSWindowController? - + func showPreview() { if let windowController = loopPreviewWindowController { windowController.window?.orderFrontRegardless() return } - + let panel = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, @@ -29,33 +29,33 @@ class PreviewController { panel.collectionBehavior = .canJoinAllSpaces panel.alphaValue = 0 panel.orderFrontRegardless() - + guard let screen = NSScreen().screenWithMouse() else { return } let menubarHeight = screen.frame.size.height - screen.visibleFrame.size.height - + let screenWidth = screen.frame.size.width let screenHeight = screen.frame.size.height - menubarHeight let screenOriginX = screen.frame.origin.x let screenOriginY = screen.frame.origin.y - + panel.setFrame(NSRect(x: screenOriginX, y: screenOriginY, width: screenWidth, height: screenHeight), display: false) - + loopPreviewWindowController = .init(window: panel) - - NSAnimationContext.runAnimationGroup({ (context) -> Void in + + NSAnimationContext.runAnimationGroup({ _ in panel.animator().alphaValue = 1 }) } - + func closePreview() { guard let windowController = loopPreviewWindowController else { return } loopPreviewWindowController = nil - + windowController.window?.animator().alphaValue = 1 - NSAnimationContext.runAnimationGroup({ (context) -> Void in + NSAnimationContext.runAnimationGroup({ _ in windowController.window?.animator().alphaValue = 0 }, completionHandler: { windowController.close() diff --git a/Loop/Preview Window/PreviewView.swift b/Loop/Preview Window/PreviewView.swift index b149a791..5cf365a5 100644 --- a/Loop/Preview Window/PreviewView.swift +++ b/Loop/Preview Window/PreviewView.swift @@ -9,79 +9,83 @@ import SwiftUI import Defaults struct PreviewView: View { - + // Used to preview inside the app's settings @State var previewMode = false - + @State var currentResizingDirection: WindowDirection = .noAction - + @Default(.useSystemAccentColor) var useSystemAccentColor @Default(.accentColor) var accentColor @Default(.useGradientAccentColor) var useGradientAccentColor @Default(.gradientAccentColor) var gradientAccentColor - + @Default(.previewVisibility) var previewVisibility @Default(.previewPadding) var previewPadding @Default(.previewCornerRadius) var previewCornerRadius @Default(.previewBorderThickness) var previewBorderThickness - + var body: some View { - VStack { - if currentResizingDirection == .bottomThird || - currentResizingDirection == .bottomHalf || - currentResizingDirection == .bottomRightQuarter || - currentResizingDirection == .bottomLeftQuarter || - currentResizingDirection == .noAction { - Rectangle() - .foregroundColor(.clear) - } - - HStack { - - if currentResizingDirection == .rightThird || - currentResizingDirection == .topRightQuarter || - currentResizingDirection == .rightHalf || - currentResizingDirection == .bottomRightQuarter || - currentResizingDirection == .noAction { + GeometryReader { geo in + VStack { + switch currentResizingDirection { + case .bottomHalf, .bottomRightQuarter, .bottomLeftQuarter, .verticalCenterThird, .bottomThird, .bottomTwoThirds, .noAction: Rectangle() - .foregroundColor(.clear) + .frame(width: currentResizingDirection == .bottomThird ? geo.size.height / 3 * 2 : nil) + default: + EmptyView() } - - ZStack { - VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) - .mask(RoundedRectangle(cornerRadius: previewCornerRadius).foregroundColor(.white)) - .shadow(radius: 10) - RoundedRectangle(cornerRadius: previewCornerRadius) - .stroke(LinearGradient( - gradient: Gradient(colors: [ - useSystemAccentColor ? Color.accentColor : accentColor, - useSystemAccentColor ? Color.accentColor : useGradientAccentColor ? gradientAccentColor : accentColor]), - startPoint: .topLeading, - endPoint: .bottomTrailing), lineWidth: previewBorderThickness) + + HStack { + switch currentResizingDirection { + case .rightHalf, .topRightQuarter, .bottomRightQuarter, .horizontalCenterThird, .rightThird, .rightTwoThirds, .noAction: + Rectangle() + .frame(width: currentResizingDirection == .rightThird ? geo.size.width / 3 * 2 : nil) + default: + EmptyView() + } + + ZStack { + VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) + .mask(RoundedRectangle(cornerRadius: previewCornerRadius).foregroundColor(.white)) + .shadow(radius: 10) + RoundedRectangle(cornerRadius: previewCornerRadius) + .stroke( + LinearGradient( + gradient: Gradient( + colors: [useSystemAccentColor ? Color.accentColor : accentColor, + useSystemAccentColor ? Color.accentColor : + (useGradientAccentColor ? gradientAccentColor : accentColor)] + ), + startPoint: .topLeading, + endPoint: .bottomTrailing + ), + lineWidth: previewBorderThickness + ) + } + .padding(previewPadding + previewBorderThickness/2) + .frame(width: currentResizingDirection == .noAction ? 0 : nil, + height: currentResizingDirection == .noAction ? 0 : nil) + + switch currentResizingDirection { + case .leftHalf, .topLeftQuarter, .bottomLeftQuarter, .horizontalCenterThird, .leftThird, .leftTwoThirds, .noAction: + Rectangle() + .frame(width: currentResizingDirection == .leftThird ? geo.size.width / 3 * 2 : nil) + default: + EmptyView() + } } - .padding(previewPadding + previewBorderThickness/2) - .frame(width: currentResizingDirection == .noAction ? 0 : nil, - height: currentResizingDirection == .noAction ? 0 : nil) - - if currentResizingDirection == .leftThird || - currentResizingDirection == .topLeftQuarter || - currentResizingDirection == .leftHalf || - currentResizingDirection == .bottomLeftQuarter || - currentResizingDirection == .noAction { + + switch currentResizingDirection { + case .topHalf, .topRightQuarter, .topLeftQuarter, .verticalCenterThird, .topThird, .topTwoThirds, .noAction: Rectangle() - .foregroundColor(.clear) + .frame(width: currentResizingDirection == .topThird ? geo.size.width / 3 * 2 : nil) + default: + EmptyView() } } - - if currentResizingDirection == .topThird || - currentResizingDirection == .topHalf || - currentResizingDirection == .topRightQuarter || - currentResizingDirection == .topLeftQuarter || - currentResizingDirection == .noAction { - Rectangle() - .foregroundColor(.clear) - } } + .foregroundColor(.clear) .opacity(currentResizingDirection == .noAction ? 0 : 1) .animation(.interpolatingSpring(stiffness: 250, damping: 25), value: currentResizingDirection) .onReceive(.currentDirectionChanged) { obj in @@ -91,7 +95,7 @@ struct PreviewView: View { } } } - + .onAppear { if previewMode { currentResizingDirection = .maximize diff --git a/Loop/Radial Menu/RadialMenuController.swift b/Loop/Radial Menu/RadialMenuController.swift index 565b92e5..cc030d5b 100644 --- a/Loop/Radial Menu/RadialMenuController.swift +++ b/Loop/Radial Menu/RadialMenuController.swift @@ -9,29 +9,29 @@ import SwiftUI import Defaults class RadialMenuController { - + let radialMenuKeybindMonitor = KeybindMonitor.shared let windowEngine = WindowEngine() let loopPreview = PreviewController() - + var currentResizingDirection: WindowDirection = .noAction - var isLoopRadialMenuShown:Bool = false + var isLoopRadialMenuShown: Bool = false var loopRadialMenuWindowController: NSWindowController? var frontmostWindow: AXUIElement? - + func showRadialMenu(frontmostWindow: AXUIElement?) { if let windowController = loopRadialMenuWindowController { windowController.window?.orderFrontRegardless() return } - + currentResizingDirection = .noAction - + let mouseX: CGFloat = NSEvent.mouseLocation.x let mouseY: CGFloat = NSEvent.mouseLocation.y - + let windowSize: CGFloat = 250 - + let panel = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, @@ -41,43 +41,55 @@ class RadialMenuController { panel.hasShadow = false panel.backgroundColor = NSColor.white.withAlphaComponent(0.00001) panel.level = .screenSaver - panel.contentView = NSHostingView(rootView: RadialMenuView(frontmostWindow: frontmostWindow, initialMousePosition: CGPoint(x: mouseX, y: mouseY))) + panel.contentView = NSHostingView( + rootView: RadialMenuView( + frontmostWindow: frontmostWindow, + initialMousePosition: CGPoint(x: mouseX, + y: mouseY) + ) + ) panel.alphaValue = 0 - panel.setFrame(CGRect(x: mouseX-windowSize/2, y: mouseY-windowSize/2, width: windowSize, height: windowSize), display: false) + panel.setFrame( + CGRect( + x: mouseX-windowSize/2, + y: mouseY-windowSize/2, + width: windowSize, + height: windowSize + ), + display: false + ) panel.orderFrontRegardless() - + loopRadialMenuWindowController = .init(window: panel) - - NSAnimationContext.runAnimationGroup({ context in + + NSAnimationContext.runAnimationGroup({ _ in panel.animator().alphaValue = 1 }) } - + private func closeRadialMenu() { guard let windowController = loopRadialMenuWindowController else { return } loopRadialMenuWindowController = nil - + windowController.window?.animator().alphaValue = 1 - NSAnimationContext.runAnimationGroup({ context in + NSAnimationContext.runAnimationGroup({ _ in windowController.window?.animator().alphaValue = 0 }, completionHandler: { windowController.close() }) } - - func AddObservers() { - + + func addObservers() { NSEvent.addGlobalMonitorForEvents(matching: NSEvent.EventTypeMask.flagsChanged) { event -> Void in - if event.keyCode == Defaults[.triggerKey] { + if event.keyCode == Defaults[.triggerKey] { if event.modifierFlags.rawValue == 256 { self.closeLoop() - } - else { + } else { self.openLoop() } } } - + NotificationCenter.default.addObserver( self, selector: #selector( @@ -88,7 +100,7 @@ class RadialMenuController { name: Notification.Name.currentDirectionChanged, object: nil ) - + NotificationCenter.default.addObserver( self, selector: #selector( @@ -100,42 +112,45 @@ class RadialMenuController { object: nil ) } - + @objc private func handleCurrentResizingDirectionChanged(notification: Notification) { if let direction = notification.userInfo?["Direction"] as? WindowDirection { currentResizingDirection = direction } } - + @objc private func closeLoopFromNotification(notification: Notification) { if let forceClosed = notification.userInfo?["wasForceClosed"] as? Bool { self.closeLoop(wasForceClosed: forceClosed) } } - + private func openLoop() { frontmostWindow = windowEngine.getFrontmostWindow() - - if Defaults[.previewVisibility] == true && frontmostWindow != nil{ + + if Defaults[.previewVisibility] == true && frontmostWindow != nil { loopPreview.showPreview() } showRadialMenu(frontmostWindow: frontmostWindow) - + radialMenuKeybindMonitor.start() - + isLoopRadialMenuShown = true } - + private func closeLoop(wasForceClosed: Bool = false) { closeRadialMenu() loopPreview.closePreview() - - if frontmostWindow != nil && wasForceClosed == false && isLoopRadialMenuShown == true && frontmostWindow != nil { + + if frontmostWindow != nil && + wasForceClosed == false && + isLoopRadialMenuShown == true && + frontmostWindow != nil { windowEngine.resize(window: frontmostWindow!, direction: currentResizingDirection) } - + radialMenuKeybindMonitor.stop() - + isLoopRadialMenuShown = false frontmostWindow = nil } diff --git a/Loop/Radial Menu/RadialMenuView.swift b/Loop/Radial Menu/RadialMenuView.swift index 8aaef713..7b733dba 100644 --- a/Loop/Radial Menu/RadialMenuView.swift +++ b/Loop/Radial Menu/RadialMenuView.swift @@ -10,51 +10,54 @@ import Combine import Defaults struct RadialMenuView: View { - - let NoActionCursorDistance: CGFloat = 8 - let RadialMenuSize: CGFloat = 100 - + + let noActionCursorDistance: CGFloat = 8 + let radialMenuSize: CGFloat = 100 + // This will determine whether Loop needs to show a warning (if it's nil) let frontmostWindow: AXUIElement? - + @State var previewMode = false @State var initialMousePosition: CGPoint = CGPoint() @State var timer = Timer.publish(every: 0.05, on: .main, in: .common).autoconnect() @State private var currentResizeDirection: WindowDirection = .noAction - + @State var angleToMouse: Angle = Angle(degrees: 0) @State var distanceToMouse: CGFloat = 0 - + // Variables that store the radial menu's shape @Default(.radialMenuCornerRadius) var radialMenuCornerRadius @Default(.radialMenuThickness) var radialMenuThickness - + // Color variables @Default(.useSystemAccentColor) var useSystemAccentColor @Default(.accentColor) var accentColor @Default(.useGradientAccentColor) var useGradientAccentColor @Default(.gradientAccentColor) var gradientAccentColor - + var body: some View { VStack { Spacer() HStack { Spacer() - + ZStack { ZStack { // NSVisualEffect on background VisualEffectView(material: .hudWindow, blendingMode: .behindWindow) - + // Used as the background when resize direction is .maximize LinearGradient( - gradient: Gradient(colors: [ - useSystemAccentColor ? Color.accentColor : accentColor, - useSystemAccentColor ? Color.accentColor : useGradientAccentColor ? gradientAccentColor : accentColor]), + gradient: Gradient( + colors: [ + useSystemAccentColor ? Color.accentColor : accentColor, + useSystemAccentColor ? Color.accentColor : useGradientAccentColor ? gradientAccentColor : accentColor] + ), startPoint: .topLeading, - endPoint: .bottomTrailing) + endPoint: .bottomTrailing + ) .opacity(currentResizeDirection == .maximize ? 1 : 0) - + // This rectangle with a gradient is masked with the current direction radial menu view Rectangle() .fill(LinearGradient( @@ -70,34 +73,33 @@ struct RadialMenuView: View { } // Mask the whole ZStack with the shape the user defines .mask { - if radialMenuCornerRadius == RadialMenuSize / 2 { + if radialMenuCornerRadius == radialMenuSize / 2 { Circle() .strokeBorder(.black, lineWidth: radialMenuThickness) - } - else { + } else { RoundedRectangle(cornerRadius: radialMenuCornerRadius, style: .continuous) .strokeBorder(.black, lineWidth: radialMenuThickness) } } - + if frontmostWindow == nil && previewMode == false { Image("custom.macwindow.trianglebadge.exclamationmark") .foregroundStyle(useSystemAccentColor ? Color.accentColor : accentColor) .font(Font.system(size: 20, weight: .bold)) } } - .frame(width: RadialMenuSize, height: RadialMenuSize) - + .frame(width: radialMenuSize, height: radialMenuSize) + Spacer() } Spacer() } .shadow(radius: 10) - + // Animate window .scaleEffect(currentResizeDirection == .maximize ? 0.85 : 1) .animation(.easeInOut, value: currentResizeDirection) - + .onAppear { if previewMode { currentResizeDirection = .topHalf @@ -105,21 +107,31 @@ struct RadialMenuView: View { } .onReceive(timer) { _ in if !previewMode { - - if (Angle(radians: initialMousePosition.angle(to: CGPoint(x: NSEvent.mouseLocation.x, y: NSEvent.mouseLocation.y))) == angleToMouse) && - (distanceToMouse == initialMousePosition.distanceSquared(to: CGPoint(x: NSEvent.mouseLocation.x, y: NSEvent.mouseLocation.y))) { + let currentAngleToMouse = Angle( + radians: initialMousePosition.angle(to: CGPoint(x: NSEvent.mouseLocation.x, + y: NSEvent.mouseLocation.y)) + ) + + let currentDistanceToMouse = initialMousePosition.distanceSquared( + to: CGPoint( + x: NSEvent.mouseLocation.x, + y: NSEvent.mouseLocation.y + ) + ) + + if (currentAngleToMouse == angleToMouse) && (currentDistanceToMouse == distanceToMouse) { return } - + // Get angle & distance to mouse - angleToMouse = Angle(radians: initialMousePosition.angle(to: CGPoint(x: NSEvent.mouseLocation.x, y: NSEvent.mouseLocation.y))) - distanceToMouse = initialMousePosition.distanceSquared(to: CGPoint(x: NSEvent.mouseLocation.x, y: NSEvent.mouseLocation.y)) - + self.angleToMouse = currentAngleToMouse + self.distanceToMouse = currentDistanceToMouse + // If mouse over 50 points away, select half or quarter positions if distanceToMouse > pow(50 - radialMenuThickness, 2) { switch Int((angleToMouse.normalized().degrees + 45 / 2) / 45) { case 0, 8: currentResizeDirection = .rightHalf - case 1: currentResizeDirection = .bottomRightQuarter + case 1: currentResizeDirection = .bottomRightQuarter case 2: currentResizeDirection = .bottomHalf case 3: currentResizeDirection = .bottomLeftQuarter case 4: currentResizeDirection = .leftHalf @@ -128,34 +140,36 @@ struct RadialMenuView: View { case 7: currentResizeDirection = .topRightQuarter default: currentResizeDirection = .noAction } - - } else if distanceToMouse < pow(NoActionCursorDistance, 2) { + } else if distanceToMouse < pow(noActionCursorDistance, 2) { currentResizeDirection = .noAction - - // Otherwise, set position to maximize } else { currentResizeDirection = .maximize } } else { currentResizeDirection = currentResizeDirection.nextWindowDirection - + if currentResizeDirection == .rightThird { currentResizeDirection = .topHalf } } } - // When current angle changes, send haptic feedback and post a notification which is used to position the preview window + // When direction changes, send haptic feedback and post a + // notification which is used to position the preview window .onChange(of: currentResizeDirection) { _ in if !previewMode { NSHapticFeedbackManager.defaultPerformer.perform( NSHapticFeedbackManager.FeedbackPattern.alignment, performanceTime: NSHapticFeedbackManager.PerformanceTime.now ) - - NotificationCenter.default.post(name: Notification.Name.currentDirectionChanged, object: nil, userInfo: ["Direction": currentResizeDirection]) + + NotificationCenter.default.post( + name: Notification.Name.currentDirectionChanged, + object: nil, + userInfo: ["Direction": currentResizeDirection] + ) } } - + .onReceive(.currentDirectionChanged) { obj in if let direction = obj.userInfo?["Direction"] as? WindowDirection { self.currentResizeDirection = direction @@ -165,11 +179,10 @@ struct RadialMenuView: View { } struct RadialMenu: View { - @Default(.radialMenuCornerRadius) var radialMenuCornerRadius - + var activeAngle: WindowDirection - + var body: some View { if radialMenuCornerRadius < 40 { // This is used when the user configures the radial menu to be a square @@ -177,19 +190,19 @@ struct RadialMenu: View { .overlay { HStack(spacing: 0) { VStack(spacing: 0) { - angleSelectorRectangle(.topLeftQuarter, activeAngle) - angleSelectorRectangle(.leftHalf, activeAngle) - angleSelectorRectangle(.bottomLeftQuarter, activeAngle) + AngleSelectorRectangle(.topLeftQuarter, activeAngle) + AngleSelectorRectangle(.leftHalf, activeAngle) + AngleSelectorRectangle(.bottomLeftQuarter, activeAngle) } VStack(spacing: 0) { - angleSelectorRectangle(.topHalf, activeAngle) + AngleSelectorRectangle(.topHalf, activeAngle) Spacer().frame(width: 100/3, height: 100/3) - angleSelectorRectangle(.bottomHalf, activeAngle) + AngleSelectorRectangle(.bottomHalf, activeAngle) } VStack(spacing: 0) { - angleSelectorRectangle(.topRightQuarter, activeAngle) - angleSelectorRectangle(.rightHalf, activeAngle) - angleSelectorRectangle(.bottomRightQuarter, activeAngle) + AngleSelectorRectangle(.topRightQuarter, activeAngle) + AngleSelectorRectangle(.rightHalf, activeAngle) + AngleSelectorRectangle(.bottomRightQuarter, activeAngle) } } } @@ -198,24 +211,24 @@ struct RadialMenu: View { // This is used when the user configures the radial menu to be a circle Color.clear .overlay { - angleSelectorCirclePart(-22.5, .rightHalf, activeAngle) - angleSelectorCirclePart(22.5, .bottomRightQuarter, activeAngle) - angleSelectorCirclePart(67.5, .bottomHalf, activeAngle) - angleSelectorCirclePart(112.5, .bottomLeftQuarter, activeAngle) - angleSelectorCirclePart(157.5, .leftHalf, activeAngle) - angleSelectorCirclePart(202.5, .topLeftQuarter, activeAngle) - angleSelectorCirclePart(247.5, .topHalf, activeAngle) - angleSelectorCirclePart(292.5, .topRightQuarter, activeAngle) + AngleSelectorCircleSegment(-22.5, .rightHalf, activeAngle) + AngleSelectorCircleSegment(22.5, .bottomRightQuarter, activeAngle) + AngleSelectorCircleSegment(67.5, .bottomHalf, activeAngle) + AngleSelectorCircleSegment(112.5, .bottomLeftQuarter, activeAngle) + AngleSelectorCircleSegment(157.5, .leftHalf, activeAngle) + AngleSelectorCircleSegment(202.5, .topLeftQuarter, activeAngle) + AngleSelectorCircleSegment(247.5, .topHalf, activeAngle) + AngleSelectorCircleSegment(292.5, .topRightQuarter, activeAngle) } } } } -struct angleSelectorRectangle: View { - +struct AngleSelectorRectangle: View { + var isActive: Bool = false var isMaximize: Bool = false - + init(_ resizePosition: WindowDirection, _ activeResizePosition: WindowDirection) { if resizePosition == activeResizePosition { isActive = true @@ -223,7 +236,7 @@ struct angleSelectorRectangle: View { isActive = false } } - + var body: some View { Rectangle() .foregroundColor(isActive ? Color.black : Color.clear) @@ -231,12 +244,12 @@ struct angleSelectorRectangle: View { } } -struct angleSelectorCirclePart: View { - +struct AngleSelectorCircleSegment: View { + var startingAngle: Double = 0 var isActive: Bool = false var isMaximize: Bool = false - + init(_ angle: Double, _ resizePosition: WindowDirection, _ activeResizePosition: WindowDirection) { startingAngle = angle if resizePosition == activeResizePosition { @@ -245,11 +258,18 @@ struct angleSelectorCirclePart: View { isActive = false } } - + var body: some View { Path { path in path.move(to: CGPoint(x: 50, y: 50)) - path.addArc(center: CGPoint(x: 50, y: 50), radius: 90, startAngle: .degrees(startingAngle), endAngle: .degrees(startingAngle+45), clockwise: false) + path.addArc( + center: CGPoint(x: 50, + y: 50), + radius: 90, + startAngle: .degrees(startingAngle), + endAngle: .degrees(startingAngle+45), + clockwise: false + ) } .foregroundColor(isActive ? Color.black : Color.clear) } diff --git a/Loop/Settings/SettingsView.swift b/Loop/Settings/SettingsView.swift index c19a928f..cfe26ace 100644 --- a/Loop/Settings/SettingsView.swift +++ b/Loop/Settings/SettingsView.swift @@ -9,15 +9,18 @@ import SwiftUI import Sparkle struct SettingsView: View { - private let updaterController: SPUStandardUpdaterController - + init() { - updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + updaterController = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: nil, + userDriverDelegate: nil + ) } - + @State var currentSettingsTab = 1 - + var body: some View { TabView(selection: $currentSettingsTab) { GeneralSettingsView() @@ -26,28 +29,28 @@ struct SettingsView: View { Image(systemName: "gear") Text("General") } - + RadialMenuSettingsView() .tag(2) .tabItem { Image("RadialMenuImage") Text("Radial Menu") } - + PreviewSettingsView() .tag(3) .tabItem { Image(systemName: "rectangle.portrait.and.arrow.right") Text("Preview") } - + KeybindingSettingsView() .tag(4) .tabItem { Image(systemName: "keyboard") Text("Keybindings") } - + MoreSettingsView(updater: updaterController.updater) .tag(5) .tabItem { diff --git a/Loop/Settings/Views/GeneralSettingsView.swift b/Loop/Settings/Views/GeneralSettingsView.swift index 1ae91dfc..2af4ce6c 100644 --- a/Loop/Settings/Views/GeneralSettingsView.swift +++ b/Loop/Settings/Views/GeneralSettingsView.swift @@ -10,9 +10,9 @@ import Defaults import ServiceManagement struct GeneralSettingsView: View { - + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - + @Default(.launchAtLogin) var launchAtLogin @Default(.isAccessibilityAccessGranted) var isAccessibilityAccessGranted @Default(.useSystemAccentColor) var useSystemAccentColor @@ -21,10 +21,10 @@ struct GeneralSettingsView: View { @Default(.gradientAccentColor) var gradientAccentColor @Default(.currentIcon) var currentIcon @Default(.timesLooped) var timesLooped - + let iconManager = IconManager() let accessibilityAccessManager = AccessibilityAccessManager() - + var body: some View { Form { Section("Behavior") { @@ -37,7 +37,7 @@ struct GeneralSettingsView: View { } } } - + Section("Loop's icon") { VStack(alignment: .leading) { Picker("Selected icon:", selection: $currentIcon) { @@ -51,21 +51,23 @@ struct GeneralSettingsView: View { .textSelection(.enabled) } } - + Section("Accent Color") { Toggle("Follow System Accent Color", isOn: $useSystemAccentColor) - + Group { ColorPicker("Accent Color", selection: $accentColor, supportsOpacity: false) Toggle("Use Gradient", isOn: $useGradientAccentColor) ColorPicker("Gradient's color", selection: $gradientAccentColor, supportsOpacity: false) .disabled(!useGradientAccentColor) - .foregroundColor(useGradientAccentColor ? (useSystemAccentColor ? .secondary : nil) : .secondary) + .foregroundColor( + useGradientAccentColor ? (useSystemAccentColor ? .secondary : nil) : .secondary + ) } .disabled(useSystemAccentColor) .foregroundColor(useSystemAccentColor ? .secondary : nil) } - + Section(content: { HStack { Text("Accessibility Access") @@ -80,9 +82,9 @@ struct GeneralSettingsView: View { }, header: { HStack { Text("Permissions") - + Spacer() - + Button("Refresh Status", action: { accessibilityAccessManager.checkAccessibilityAccess(ask: true) }) diff --git a/Loop/Settings/Views/KeybindingSettingsView.swift b/Loop/Settings/Views/KeybindingSettingsView.swift index 2f88346e..7dcb9812 100644 --- a/Loop/Settings/Views/KeybindingSettingsView.swift +++ b/Loop/Settings/Views/KeybindingSettingsView.swift @@ -9,28 +9,30 @@ import SwiftUI import Defaults struct KeybindingSettingsView: View { - + @Default(.triggerKey) var triggerKey @Default(.useSystemAccentColor) var useSystemAccentColor @Default(.accentColor) var accentColor - - let LoopTriggerKeyOptions = LoopTriggerKeys.options - @State var triggerKeySymbol: String = "custom.globe.rectangle.fill" // This is just a placeholder, but it's a valid image - + + let loopTriggerKeyOptions = LoopTriggerKeys.options + + // This is just a placeholder, but it's a valid image + @State var triggerKeySymbol: String = "custom.globe.rectangle.fill" + var body: some View { Form { Section("Keybindings") { VStack(alignment: .leading) { Picker("Trigger Loop", selection: $triggerKey) { - ForEach(0..