From 8e19cfcafb0dd0845775e7013b6bca742f89e1b2 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Fri, 21 Jun 2024 16:47:45 -0600 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Convert=20values=20directly=20i?= =?UTF-8?q?n=20`AXUIElement+Extensions`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Extensions/AXUIElement+Extensions.swift | 102 +++++++++++++------ Loop/Window Management/Window.swift | 38 ++++--- Loop/Window Management/WindowEngine.swift | 4 +- 3 files changed, 97 insertions(+), 47 deletions(-) diff --git a/Loop/Extensions/AXUIElement+Extensions.swift b/Loop/Extensions/AXUIElement+Extensions.swift index 22ee10c5..3533d4c4 100644 --- a/Loop/Extensions/AXUIElement+Extensions.swift +++ b/Loop/Extensions/AXUIElement+Extensions.swift @@ -10,36 +10,29 @@ import SwiftUI extension AXUIElement { static let systemWide = AXUIElementCreateSystemWide() - func getValue(_ attribute: NSAccessibility.Attribute) -> AnyObject? { + func getValue(_ attribute: NSAccessibility.Attribute) -> T? { var value: AnyObject? let result = AXUIElementCopyAttributeValue(self, attribute as CFString, &value) - if result == .success { - return value + + if result == .noValue || result == .attributeUnsupported { + return nil } - return nil - } - @discardableResult - func setValue(_ attribute: NSAccessibility.Attribute, value: AnyObject) -> Bool { - let result = AXUIElementSetAttributeValue(self, attribute as CFString, value) - return result == .success - } + guard result == .success else { + return nil + } - @discardableResult - func setValue(_ attribute: NSAccessibility.Attribute, value: Bool) -> Bool { - setValue(attribute, value: value as CFBoolean) - } + guard let unpackedValue = (unpackAXValue(value!) as? T) else { + return nil + } - @discardableResult - func setValue(_ attribute: NSAccessibility.Attribute, value: CGPoint) -> Bool { - guard let axValue = AXValue.from(value: value, type: .cgPoint) else { return false } - return setValue(attribute, value: axValue) + return unpackedValue } @discardableResult - func setValue(_ attribute: NSAccessibility.Attribute, value: CGSize) -> Bool { - guard let axValue = AXValue.from(value: value, type: .cgSize) else { return false } - return setValue(attribute, value: axValue) + func setValue(_ attribute: NSAccessibility.Attribute, value: Any) -> Bool { + let result = AXUIElementSetAttributeValue(self, attribute as CFString, packAXValue(value)) + return result == .success } func performAction(_ action: String) { @@ -62,6 +55,65 @@ extension AXUIElement { } return nil } + + private func packAXValue(_ value: Any) -> AnyObject { + switch value { + case let val as Window: + return val.axWindow + case let val as Bool: + return val as CFBoolean + case var val as CFRange: + return AXValueCreate(AXValueType(rawValue: kAXValueCFRangeType)!, &val)! + case var val as CGPoint: + return AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &val)! + case var val as CGRect: + return AXValueCreate(AXValueType(rawValue: kAXValueCGRectType)!, &val)! + case var val as CGSize: + return AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &val)! + default: + return value as AnyObject + } + } + + private func unpackAXValue(_ value: AnyObject) -> Any { + switch CFGetTypeID(value) { + case AXUIElementGetTypeID(): + return value as! AXUIElement + case AXValueGetTypeID(): + let type = AXValueGetType(value as! AXValue) + switch type { + case .axError: + var result: AXError = .success + let success = AXValueGetValue(value as! AXValue, type, &result) + assert(success) + return result + case .cfRange: + var result: CFRange = CFRange() + let success = AXValueGetValue(value as! AXValue, type, &result) + assert(success) + return result + case .cgPoint: + var result: CGPoint = CGPoint.zero + let success = AXValueGetValue(value as! AXValue, type, &result) + assert(success) + return result + case .cgRect: + var result: CGRect = CGRect.zero + let success = AXValueGetValue(value as! AXValue, type, &result) + assert(success) + return result + case .cgSize: + var result: CGSize = CGSize.zero + let success = AXValueGetValue(value as! AXValue, type, &result) + assert(success) + return result + default: + return value + } + default: + return value + } + } } extension NSAccessibility.Attribute { @@ -69,11 +121,3 @@ extension NSAccessibility.Attribute { static let enhancedUserInterface = NSAccessibility.Attribute(rawValue: "AXEnhancedUserInterface") static let windowIds = NSAccessibility.Attribute(rawValue: "AXWindowsIDs") } - -extension AXValue { - static func from(value: Any, type: AXValueType) -> AXValue? { - withUnsafePointer(to: value) { ptr in - AXValueCreate(type, ptr) - } - } -} diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index 41bd907a..f326d840 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -48,8 +48,10 @@ class Window { convenience init?(pid: pid_t) { let element = AXUIElementCreateApplication(pid) - guard let window = element.getValue(.focusedWindow) else { return nil } - self.init(element: window as! AXUIElement) + guard let window: AXUIElement = element.getValue(.focusedWindow) else { + return nil + } + self.init(element: window) } func getPid() -> pid_t? { @@ -60,24 +62,28 @@ class Window { } var role: NSAccessibility.Role? { - guard let value = self.axWindow.getValue(.role) as? String else { return nil } + guard let value: String = self.axWindow.getValue(.role) else { + return nil + } return NSAccessibility.Role(rawValue: value) } var subrole: NSAccessibility.Subrole? { - guard let value = self.axWindow.getValue(.subrole) as? String else { return nil } + guard let value: String = self.axWindow.getValue(.subrole) else { + return nil + } return NSAccessibility.Subrole(rawValue: value) } var title: String? { - self.axWindow.getValue(.title) as? String + self.axWindow.getValue(.title) } var enhancedUserInterface: Bool? { get { guard let pid = self.getPid() else { return nil } let appWindow = AXUIElementCreateApplication(pid) - return appWindow.getValue(.enhancedUserInterface) as? Bool + return appWindow.getValue(.enhancedUserInterface) } set { guard @@ -107,7 +113,7 @@ class Window { } var isFullscreen: Bool { - let result = self.axWindow.getValue(.fullScreen) as? NSNumber + let result: NSNumber? = self.axWindow.getValue(.fullScreen) return result?.boolValue ?? false } @@ -148,7 +154,7 @@ class Window { } var isMinimized: Bool { - let result = self.axWindow.getValue(.minimized) as? NSNumber + let result: NSNumber? = self.axWindow.getValue(.minimized) return result?.boolValue ?? false } @@ -166,10 +172,10 @@ class Window { } var position: CGPoint { - var point: CGPoint = .zero - guard let value = self.axWindow.getValue(.position) else { return point } - AXValueGetValue(value as! AXValue, .cgPoint, &point) // Convert to CGPoint - return point + guard let result: CGPoint = self.axWindow.getValue(.position) else { + return .zero + } + return result } @discardableResult @@ -178,10 +184,10 @@ class Window { } var size: CGSize { - var size: CGSize = .zero - guard let value = self.axWindow.getValue(.size) else { return size } - AXValueGetValue(value as! AXValue, .cgSize, &size) // Convert to CGSize - return size + guard let result: CGSize = self.axWindow.getValue(.size) else { + return .zero + } + return result } @discardableResult diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index 5dcef5e1..98200572 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -139,8 +139,8 @@ enum WindowEngine { static func windowAtPosition(_ position: CGPoint) -> Window? { if let element = AXUIElement.systemWide.getElementAtPosition(position), - let windowElement = element.getValue(.window), - let window = Window(element: windowElement as! AXUIElement) { + let windowElement: AXUIElement = element.getValue(.window), + let window = Window(element: windowElement) { return window } From dcaf06c7aeb921453c48fd5cd58a018523f48808 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Fri, 21 Jun 2024 16:56:23 -0600 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=8E=A8=20Put=20`getPID()`=20inside=20?= =?UTF-8?q?`AXUIElement+Extensions`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Extensions/AXUIElement+Extensions.swift | 11 +++++++++++ Loop/Window Management/Window.swift | 17 ++++------------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Loop/Extensions/AXUIElement+Extensions.swift b/Loop/Extensions/AXUIElement+Extensions.swift index 3533d4c4..51fa6564 100644 --- a/Loop/Extensions/AXUIElement+Extensions.swift +++ b/Loop/Extensions/AXUIElement+Extensions.swift @@ -114,6 +114,17 @@ extension AXUIElement { return value } } + + func getPID() -> pid_t? { + var pid: pid_t = 0 + let result = AXUIElementGetPid(self, &pid) + + guard result == .success else { + return nil + } + + return pid + } } extension NSAccessibility.Attribute { diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index f326d840..661f0237 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -19,9 +19,7 @@ class Window { init?(element: AXUIElement) { self.axWindow = element - var pid = pid_t(0) - _ = AXUIElementGetPid(self.axWindow, &pid) - + let pid = axWindow.getPID() self.nsRunningApplication = NSWorkspace.shared.runningApplications.first { $0.processIdentifier == pid } @@ -54,13 +52,6 @@ class Window { self.init(element: window) } - func getPid() -> pid_t? { - var pid = pid_t(0) - let result = AXUIElementGetPid(self.axWindow, &pid) - guard result == .success else { return nil } - return pid - } - var role: NSAccessibility.Role? { guard let value: String = self.axWindow.getValue(.role) else { return nil @@ -81,14 +72,14 @@ class Window { var enhancedUserInterface: Bool? { get { - guard let pid = self.getPid() else { return nil } + guard let pid = axWindow.getPID() else { return nil } let appWindow = AXUIElementCreateApplication(pid) return appWindow.getValue(.enhancedUserInterface) } set { - guard + guard let newValue, - let pid = self.getPid() + let pid = axWindow.getPID() else { return } From 97f0db8425dde6a7ed019faadc8b9ae7a5550816 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Fri, 21 Jun 2024 17:33:18 -0600 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=A5=85=20Use=20errors=20instead=20of?= =?UTF-8?q?=20returning=20`nil`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/AppDelegate+UNNotifications.swift | 2 +- Loop/Extensions/AXUIElement+Extensions.swift | 57 +++-- .../UNNotification+Extensions.swift | 2 +- .../Behavior/BehaviorConfiguration.swift | 12 +- Loop/Managers/WindowDragManager.swift | 22 +- Loop/MenuBar/MenuBarResizeButton.swift | 2 +- Loop/Updater/Updater.swift | 8 +- Loop/Window Management/Window.swift | 225 +++++++++++------- .../Window Management/WindowAction+Port.swift | 8 +- Loop/Window Management/WindowEngine.swift | 53 +++-- .../WindowTransformAnimation.swift | 4 +- 11 files changed, 246 insertions(+), 149 deletions(-) diff --git a/Loop/AppDelegate+UNNotifications.swift b/Loop/AppDelegate+UNNotifications.swift index a4d0d43b..ddff31f0 100644 --- a/Loop/AppDelegate+UNNotifications.swift +++ b/Loop/AppDelegate+UNNotifications.swift @@ -40,7 +40,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { } if let error { - print(error) + print(error.localizedDescription) } } } diff --git a/Loop/Extensions/AXUIElement+Extensions.swift b/Loop/Extensions/AXUIElement+Extensions.swift index 51fa6564..e5882d9b 100644 --- a/Loop/Extensions/AXUIElement+Extensions.swift +++ b/Loop/Extensions/AXUIElement+Extensions.swift @@ -10,39 +10,41 @@ import SwiftUI extension AXUIElement { static let systemWide = AXUIElementCreateSystemWide() - func getValue(_ attribute: NSAccessibility.Attribute) -> T? { + func getValue(_ attribute: NSAccessibility.Attribute) throws -> T? { var value: AnyObject? - let result = AXUIElementCopyAttributeValue(self, attribute as CFString, &value) - - if result == .noValue || result == .attributeUnsupported { + let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &value) + + if error == .noValue || error == .attributeUnsupported { return nil } - guard result == .success else { - return nil + guard error == .success else { + throw error } guard let unpackedValue = (unpackAXValue(value!) as? T) else { - return nil + throw AXError.illegalArgument } return unpackedValue } - @discardableResult - func setValue(_ attribute: NSAccessibility.Attribute, value: Any) -> Bool { - let result = AXUIElementSetAttributeValue(self, attribute as CFString, packAXValue(value)) - return result == .success - } + func setValue(_ attribute: NSAccessibility.Attribute, value: Any) throws { + let error = AXUIElementSetAttributeValue(self, attribute as CFString, packAXValue(value)) - func performAction(_ action: String) { - AXUIElementPerformAction(self, action as CFString) + guard error == .success else { + throw error + } } - func getElementAtPosition(_ position: CGPoint) -> AXUIElement? { + func getElementAtPosition(_ position: CGPoint) throws -> AXUIElement? { var element: AXUIElement? - let result = AXUIElementCopyElementAtPosition(self, Float(position.x), Float(position.y), &element) - guard result == .success else { return nil } + let error = AXUIElementCopyElementAtPosition(self, Float(position.x), Float(position.y), &element) + + guard error == .success else { + throw error + } + return element } @@ -115,18 +117,31 @@ extension AXUIElement { } } - func getPID() -> pid_t? { + func getPID() throws -> pid_t? { var pid: pid_t = 0 - let result = AXUIElementGetPid(self, &pid) + let error = AXUIElementGetPid(self, &pid) - guard result == .success else { - return nil + guard error == .success else { + throw error } return pid } + + func getWindowID() throws -> CGWindowID { + var id: CGWindowID = 0 + let error = _AXUIElementGetWindow(self, &id) + + guard error == .success else { + throw error + } + + return id + } } +extension AXError: Swift.Error {} + extension NSAccessibility.Attribute { static let fullScreen: NSAccessibility.Attribute = .init(rawValue: "AXFullScreen") static let enhancedUserInterface = NSAccessibility.Attribute(rawValue: "AXEnhancedUserInterface") diff --git a/Loop/Extensions/UNNotification+Extensions.swift b/Loop/Extensions/UNNotification+Extensions.swift index 76328c1d..e92598f0 100644 --- a/Loop/Extensions/UNNotification+Extensions.swift +++ b/Loop/Extensions/UNNotification+Extensions.swift @@ -31,7 +31,7 @@ extension UNNotificationAttachment { ) return imageAttachment } catch { - print("error \(error)") + print("error \(error.localizedDescription)") } return nil diff --git a/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift b/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift index e9d3b499..e42028e3 100644 --- a/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift +++ b/Loop/Luminare/Settings/Behavior/BehaviorConfiguration.swift @@ -15,10 +15,14 @@ class BehaviorConfigurationModel: ObservableObject { didSet { Defaults[.launchAtLogin] = launchAtLogin - if launchAtLogin { - try? SMAppService().register() - } else { - try? SMAppService().unregister() + do { + if launchAtLogin { + try SMAppService().register() + } else { + try SMAppService().unregister() + } + } catch { + print("Failed to \(launchAtLogin ? "register" : "unregister") login item: \(error.localizedDescription)") } } } diff --git a/Loop/Managers/WindowDragManager.swift b/Loop/Managers/WindowDragManager.swift index ce3be260..aa008cb5 100644 --- a/Loop/Managers/WindowDragManager.swift +++ b/Loop/Managers/WindowDragManager.swift @@ -72,15 +72,19 @@ class WindowDragManager { let mousePosition = NSEvent.mouseLocation.flipY(screen: screen) - guard - let draggingWindow = WindowEngine.windowAtPosition(mousePosition), - !draggingWindow.isAppExcluded - else { - return - } + do { + guard + let draggingWindow = try WindowEngine.windowAtPosition(mousePosition), + !draggingWindow.isAppExcluded + else { + return + } - self.draggingWindow = draggingWindow - initialWindowFrame = draggingWindow.frame + self.draggingWindow = draggingWindow + initialWindowFrame = draggingWindow.frame + } catch { + print("Failed to get window at position: \(error.localizedDescription)") + } } private func hasWindowMoved(_ windowFrame: CGRect, _ initialFrame: CGRect) -> Bool { @@ -103,7 +107,7 @@ class WindowDragManager { newWindowFrame = newWindowFrame.pushBottomRightPointInside(screen.frame) window.setFrame(newWindowFrame) } else { - window.setSize(initialFrame.size) + window.size = initialFrame.size } // If the window doesn't contain the cursor, keep the original maxX diff --git a/Loop/MenuBar/MenuBarResizeButton.swift b/Loop/MenuBar/MenuBarResizeButton.swift index 83450ec2..714f6b01 100644 --- a/Loop/MenuBar/MenuBarResizeButton.swift +++ b/Loop/MenuBar/MenuBarResizeButton.swift @@ -16,7 +16,7 @@ struct MenuBarResizeButton: View { var body: some View { Button { - if let frontmostWindow = WindowEngine.frontmostWindow, + if let frontmostWindow = try? WindowEngine.getFrontmostWindow(), let screen = NSScreen.main { WindowEngine.resize(frontmostWindow, to: .init(direction), on: screen) } diff --git a/Loop/Updater/Updater.swift b/Loop/Updater/Updater.swift index b52e8c2a..5fb2c079 100755 --- a/Loop/Updater/Updater.swift +++ b/Loop/Updater/Updater.swift @@ -185,13 +185,17 @@ class Updater: ObservableObject { DispatchQueue.main.async { self.progressBar = 0.8 - try? fileManager.removeItem(atPath: fileURL) // Clean up the zip file after extraction. + do { + try fileManager.removeItem(atPath: fileURL) // Clean up the zip file after extraction. + } catch { + print("Failed to delete downloaded file: \(error.localizedDescription)") + } self.progressBar = 1.0 self.updateState = .unavailable // Update the state to reflect that the update has been applied. } } catch { DispatchQueue.main.async { - NSLog("Error updating the app: \(error)") + NSLog("Error updating the app: \(error.localizedDescription)") } } } diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index 661f0237..db96fa4b 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -11,87 +11,120 @@ import SwiftUI @_silgen_name("_AXUIElementGetWindow") @discardableResult func _AXUIElementGetWindow(_ axUiElement: AXUIElement, _ wid: inout CGWindowID) -> AXError +enum WindowError: Error { + case invalidWindow + + var localizedDescription: String { + switch self { + case .invalidWindow: + "Invalid window" + } + } +} + class Window { let axWindow: AXUIElement let cgWindowID: CGWindowID let nsRunningApplication: NSRunningApplication? - init?(element: AXUIElement) { + init(element: AXUIElement) throws { self.axWindow = element - let pid = axWindow.getPID() + let pid = try axWindow.getPID() self.nsRunningApplication = NSWorkspace.shared.runningApplications.first { $0.processIdentifier == pid } - // Set self's CGWindowID - var windowId = CGWindowID(0) - let result = _AXUIElementGetWindow(self.axWindow, &windowId) - guard result == .success else { return nil } - self.cgWindowID = windowId + self.cgWindowID = try axWindow.getWindowID() if self.role != .window, self.subrole != .standardWindow { - print("This is an invalid window") - return nil + throw WindowError.invalidWindow } // Check if this is a widget if let title = nsRunningApplication?.localizedName, title == "Notification Center" { - print("This window is a part of Notification Center") - return nil + throw WindowError.invalidWindow } } - convenience init?(pid: pid_t) { + convenience init(pid: pid_t) throws { let element = AXUIElementCreateApplication(pid) - guard let window: AXUIElement = element.getValue(.focusedWindow) else { - return nil + guard let window: AXUIElement = try element.getValue(.focusedWindow) else { + throw WindowError.invalidWindow } - self.init(element: window) + try self.init(element: window) } var role: NSAccessibility.Role? { - guard let value: String = self.axWindow.getValue(.role) else { + do { + guard let value: String = try self.axWindow.getValue(.role) else { + return nil + } + return NSAccessibility.Role(rawValue: value) + } catch { + print("Failed to get role: \(error.localizedDescription)") return nil } - return NSAccessibility.Role(rawValue: value) } var subrole: NSAccessibility.Subrole? { - guard let value: String = self.axWindow.getValue(.subrole) else { + do { + guard let value: String = try self.axWindow.getValue(.subrole) else { + return nil + } + return NSAccessibility.Subrole(rawValue: value) + } catch { + print("Failed to get subrole: \(error.localizedDescription)") return nil } - return NSAccessibility.Subrole(rawValue: value) } var title: String? { - self.axWindow.getValue(.title) + do { + return try self.axWindow.getValue(.title) + } catch { + print("Failed to get title: \(error.localizedDescription)") + return nil + } } - var enhancedUserInterface: Bool? { + var enhancedUserInterface: Bool { get { - guard let pid = axWindow.getPID() else { return nil } - let appWindow = AXUIElementCreateApplication(pid) - return appWindow.getValue(.enhancedUserInterface) + do { + guard let pid = try axWindow.getPID() else { + return false + } + let appWindow = AXUIElementCreateApplication(pid) + let result: Bool? = try appWindow.getValue(.enhancedUserInterface) + return result ?? false + } catch { + print("Failed to get enhancedUserInterface: \(error.localizedDescription)") + return false + } } set { - guard - let newValue, - let pid = axWindow.getPID() - else { - return + do { + guard let pid = try axWindow.getPID() else { + return + } + let appWindow = AXUIElementCreateApplication(pid) + try appWindow.setValue(.enhancedUserInterface, value: newValue) + } catch { + print("Failed to set enhancedUserInterface: \(error.localizedDescription)") } - let appWindow = AXUIElementCreateApplication(pid) - appWindow.setValue(.enhancedUserInterface, value: newValue) } } func activate() { - self.axWindow.setValue(.main, value: true) - if let runningApplication = self.nsRunningApplication { - runningApplication.activate() + do { + try self.axWindow.setValue(.main, value: true) + if let runningApplication = self.nsRunningApplication { + runningApplication.activate() + } + } catch { + print("Failed to activate window: \(error.localizedDescription)") } } @@ -103,22 +136,27 @@ class Window { return false } - var isFullscreen: Bool { - let result: NSNumber? = self.axWindow.getValue(.fullScreen) - return result?.boolValue ?? false - } - - @discardableResult - func setFullscreen(_ state: Bool) -> Bool { - self.axWindow.setValue(.fullScreen, value: state) + var fullscreen: Bool { + get { + do { + let result: NSNumber? = try self.axWindow.getValue(.fullScreen) + return result?.boolValue ?? false + } catch { + print("Failed to get fullscreen: \(error.localizedDescription)") + return false + } + } + set { + do { + try self.axWindow.setValue(.fullScreen, value: newValue) + } catch { + print("Failed to set fullscreen: \(error.localizedDescription)") + } + } } - @discardableResult - func toggleFullscreen() -> Bool { - if !self.isFullscreen { - return self.setFullscreen(true) - } - return self.setHidden(false) + func toggleFullscreen() { + fullscreen = !fullscreen } var isHidden: Bool { @@ -144,46 +182,69 @@ class Window { return self.setHidden(false) } - var isMinimized: Bool { - let result: NSNumber? = self.axWindow.getValue(.minimized) - return result?.boolValue ?? false - } - - @discardableResult - func setMinimized(_ state: Bool) -> Bool { - self.axWindow.setValue(.minimized, value: state) + var minimized: Bool { + get { + do { + let result: NSNumber? = try self.axWindow.getValue(.minimized) + return result?.boolValue ?? false + } catch { + print("Failed to get minimized: \(error.localizedDescription)") + return false + } + } + set { + do { + try self.axWindow.setValue(.minimized, value: newValue) + } catch { + print("Failed to set minimized: \(error.localizedDescription)") + } + } } - @discardableResult - func toggleMinimized() -> Bool { - if !self.isMinimized { - return self.setMinimized(true) - } - return self.setMinimized(false) + func toggleMinimized() { + minimized = !minimized } var position: CGPoint { - guard let result: CGPoint = self.axWindow.getValue(.position) else { - return .zero + get { + do { + guard let result: CGPoint = try self.axWindow.getValue(.position) else { + return .zero + } + return result + } catch { + print("Failed to get position: \(error.localizedDescription)") + return .zero + } + } + set { + do { + try self.axWindow.setValue(.position, value: newValue) + } catch { + print("Failed to set position: \(error.localizedDescription)") + } } - return result - } - - @discardableResult - func setPosition(_ position: CGPoint) -> Bool { - self.axWindow.setValue(.position, value: position) } var size: CGSize { - guard let result: CGSize = self.axWindow.getValue(.size) else { - return .zero + get { + do { + guard let result: CGSize = try self.axWindow.getValue(.size) else { + return .zero + } + return result + } catch { + print("Failed to get size: \(error.localizedDescription)") + return .zero + } + } + set { + do { + try self.axWindow.setValue(.size, value: newValue) + } catch { + print("Failed to set size: \(error.localizedDescription)") + } } - return result - } - - @discardableResult - func setSize(_ size: CGSize) -> Bool { - self.axWindow.setValue(.size, value: size) } var frame: CGRect { @@ -197,7 +258,7 @@ class Window { bounds: CGRect = .zero, // Only does something when window animations are on completionHandler: @escaping (() -> ()) = {} ) { - let enhancedUI = self.enhancedUserInterface ?? false + let enhancedUI = self.enhancedUserInterface if enhancedUI { let appName = nsRunningApplication?.localizedName @@ -215,10 +276,10 @@ class Window { animation.startInBackground() } else { if sizeFirst { - self.setSize(rect.size) + self.size = rect.size } - self.setPosition(rect.origin) - self.setSize(rect.size) + self.position = rect.origin + self.size = rect.size completionHandler() } diff --git a/Loop/Window Management/WindowAction+Port.swift b/Loop/Window Management/WindowAction+Port.swift index ead70575..0a7521bb 100644 --- a/Loop/Window Management/WindowAction+Port.swift +++ b/Loop/Window Management/WindowAction+Port.swift @@ -87,7 +87,7 @@ extension WindowAction { attemptSave(of: json) } } catch { - print("Error encoding keybinds: \(error)") + print("Error encoding keybinds: \(error.localizedDescription)") } } @@ -109,7 +109,7 @@ extension WindowAction { do { try data?.write(to: destUrl) } catch { - print("Error writing to file: \(error)") + print("Error writing to file: \(error.localizedDescription)") } } } @@ -128,7 +128,7 @@ extension WindowAction { let jsonString = try String(contentsOf: selectedFileURL) importKeybinds(from: jsonString) } catch { - print("Error reading file: \(error)") + print("Error reading file: \(error.localizedDescription)") } } } @@ -169,7 +169,7 @@ extension WindowAction { } } } catch { - print("Error decoding keybinds: \(error)") + print("Error decoding keybinds: \(error.localizedDescription)") let alert = NSAlert() alert.messageText = "Error Reading Keybinds" diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index 98200572..627f37d0 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -39,7 +39,7 @@ enum WindowEngine { WindowRecords.record(window, action) return } - window.setFullscreen(false) + window.fullscreen = false if action.direction == .hide { window.toggleHidden() @@ -59,7 +59,7 @@ enum WindowEngine { print("Target window frame: \(targetFrame)") - let enhancedUI = window.enhancedUserInterface ?? false + let enhancedUI = window.enhancedUserInterface let animate = Defaults[.animateWindowResizes] && !enhancedUI WindowRecords.record(window, action) @@ -112,14 +112,23 @@ enum WindowEngine { static func getTargetWindow() -> Window? { var result: Window? - if Defaults[.resizeWindowUnderCursor], - let mouseLocation = CGEvent.mouseLocation, - let window = WindowEngine.windowAtPosition(mouseLocation) { - result = window + do { + if Defaults[.resizeWindowUnderCursor], + let mouseLocation = CGEvent.mouseLocation, + let window = try WindowEngine.windowAtPosition(mouseLocation) { + result = window + } + } catch { + print("Failed to get window at cursor: \(error.localizedDescription)") } + if result == nil { - result = WindowEngine.frontmostWindow + do { + result = try WindowEngine.getFrontmostWindow() + } catch { + print("Failed to get frontmost window: \(error.localizedDescription)") + } } return result @@ -127,21 +136,17 @@ enum WindowEngine { /// Get the frontmost Window /// - Returns: Window? - static var frontmostWindow: Window? { - guard - let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive }), - let window = Window(pid: app.processIdentifier) - else { + static func getFrontmostWindow() throws -> Window? { + guard let app = NSWorkspace.shared.runningApplications.first(where: { $0.isActive }) else { return nil } - return window + return try Window(pid: app.processIdentifier) } - static func windowAtPosition(_ position: CGPoint) -> Window? { - if let element = AXUIElement.systemWide.getElementAtPosition(position), - let windowElement: AXUIElement = element.getValue(.window), - let window = Window(element: windowElement) { - return window + static func windowAtPosition(_ position: CGPoint) throws -> Window? { + if let element = try AXUIElement.systemWide.getElementAtPosition(position), + let windowElement: AXUIElement = try element.getValue(.window) { + return try Window(element: windowElement) } let windowList = WindowEngine.windowList @@ -162,9 +167,13 @@ enum WindowEngine { var windowList: [Window] = [] for window in list { - if let pid = window[kCGWindowOwnerPID as String] as? Int32, - let window = Window(pid: pid) { - windowList.append(window) + if let pid = window[kCGWindowOwnerPID as String] as? Int32 { + do { + let window = try Window(pid: pid) + windowList.append(window) + } catch { + print("Failed to create window: \(error.localizedDescription)") + } } } @@ -205,6 +214,6 @@ enum WindowEngine { fixedWindowFrame.origin.y = bounds.maxY - fixedWindowFrame.height - bottomPadding } - window.setPosition(fixedWindowFrame.origin) + window.position = fixedWindowFrame.origin } } diff --git a/Loop/Window Management/WindowTransformAnimation.swift b/Loop/Window Management/WindowTransformAnimation.swift index 5f238779..ea947907 100644 --- a/Loop/Window Management/WindowTransformAnimation.swift +++ b/Loop/Window Management/WindowTransformAnimation.swift @@ -78,8 +78,8 @@ class WindowTransformAnimation: NSAnimation { } } - window.setPosition(newFrame.origin) - window.setSize(newFrame.size) + window.position = newFrame.origin + window.size = newFrame.size lastWindowFrame = window.frame if currentProgress >= 1.0 { From b0c2c9c3988cb45a6f0024018dfc648262bf663c Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Fri, 21 Jun 2024 18:03:50 -0600 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=8E=A8=20Format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop.xcodeproj/project.pbxproj | 4 ---- Loop/Extensions/AXUIElement+Extensions.swift | 22 ++++++++--------- Loop/Managers/WindowDragManager.swift | 4 +--- Loop/Utilities/grid.metal | 25 -------------------- Loop/Window Management/WindowEngine.swift | 1 - 5 files changed, 12 insertions(+), 44 deletions(-) delete mode 100644 Loop/Utilities/grid.metal diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 3376f77a..82ead96d 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ A80397D22A93287C006D2796 /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = A80397D12A93287C006D2796 /* MenuBarExtraAccess */; settings = {ATTRIBUTES = (Required, ); }; }; A80397D42A932993006D2796 /* MenuBarIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A80397D32A932993006D2796 /* MenuBarIconView.swift */; }; A8055EC22AFEDE0B00459D13 /* Keycorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8055EC12AFEDE0B00459D13 /* Keycorder.swift */; }; - A8063A732B19891900EAB3D9 /* grid.metal in Sources */ = {isa = PBXBuildFile; fileRef = A8063A722B19891900EAB3D9 /* grid.metal */; }; 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 */; }; @@ -103,7 +102,6 @@ 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 = ""; }; - A8063A722B19891900EAB3D9 /* grid.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = grid.metal; 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 = ""; }; @@ -222,7 +220,6 @@ A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */, A8D6D3002B6C894C0061B11F /* PaddingModel.swift */, A8D6D3042B6C92F20061B11F /* WallpaperView.swift */, - A8063A722B19891900EAB3D9 /* grid.metal */, ); path = Utilities; sourceTree = ""; @@ -620,7 +617,6 @@ A81989082AC8F2AE00EFF7A1 /* MenuBarResizeButton.swift in Sources */, A87DDD152B50A6A400A32C76 /* ScreenManager.swift in Sources */, A86949862A8F2BB70051AAAF /* CGKeyCode+Extensions.swift in Sources */, - A8063A732B19891900EAB3D9 /* grid.metal in Sources */, A8F0125B2AEDD7660017307F /* WindowAction.swift in Sources */, A8BC77792C0EB4DD008E2EDA /* AppDelegate+UNNotifications.swift in Sources */, A80397D42A932993006D2796 /* MenuBarIconView.swift in Sources */, diff --git a/Loop/Extensions/AXUIElement+Extensions.swift b/Loop/Extensions/AXUIElement+Extensions.swift index e5882d9b..1239463f 100644 --- a/Loop/Extensions/AXUIElement+Extensions.swift +++ b/Loop/Extensions/AXUIElement+Extensions.swift @@ -61,19 +61,19 @@ extension AXUIElement { private func packAXValue(_ value: Any) -> AnyObject { switch value { case let val as Window: - return val.axWindow + val.axWindow case let val as Bool: - return val as CFBoolean + val as CFBoolean case var val as CFRange: - return AXValueCreate(AXValueType(rawValue: kAXValueCFRangeType)!, &val)! + AXValueCreate(AXValueType(rawValue: kAXValueCFRangeType)!, &val)! case var val as CGPoint: - return AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &val)! + AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &val)! case var val as CGRect: - return AXValueCreate(AXValueType(rawValue: kAXValueCGRectType)!, &val)! + AXValueCreate(AXValueType(rawValue: kAXValueCGRectType)!, &val)! case var val as CGSize: - return AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &val)! + AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &val)! default: - return value as AnyObject + value as AnyObject } } @@ -90,22 +90,22 @@ extension AXUIElement { assert(success) return result case .cfRange: - var result: CFRange = CFRange() + var result = CFRange() let success = AXValueGetValue(value as! AXValue, type, &result) assert(success) return result case .cgPoint: - var result: CGPoint = CGPoint.zero + var result = CGPoint.zero let success = AXValueGetValue(value as! AXValue, type, &result) assert(success) return result case .cgRect: - var result: CGRect = CGRect.zero + var result = CGRect.zero let success = AXValueGetValue(value as! AXValue, type, &result) assert(success) return result case .cgSize: - var result: CGSize = CGSize.zero + var result = CGSize.zero let success = AXValueGetValue(value as! AXValue, type, &result) assert(success) return result diff --git a/Loop/Managers/WindowDragManager.swift b/Loop/Managers/WindowDragManager.swift index aa008cb5..b125d6ef 100644 --- a/Loop/Managers/WindowDragManager.swift +++ b/Loop/Managers/WindowDragManager.swift @@ -182,9 +182,7 @@ class WindowDragManager { } private func attemptWindowSnap(_ window: Window) { - guard - let screen = NSScreen.screenWithMouse - else { + guard let screen = NSScreen.screenWithMouse else { return } diff --git a/Loop/Utilities/grid.metal b/Loop/Utilities/grid.metal deleted file mode 100644 index a6232024..00000000 --- a/Loop/Utilities/grid.metal +++ /dev/null @@ -1,25 +0,0 @@ -// -// grid.metal -// Loop -// -// Created by Kai Azim on 2023-11-30. -// - -#include -using namespace metal; - -[[stitchable]] half4 grid(float2 position, half4 currentColor, float size, half4 newColor) { - position += 1; - - // Calculate the position of the current pixel in grid coordinates. - uint2 gridPosition = uint2(position / size); - - // Introduce a factor to control the thickness of the grid lines - float thicknessFactor = 0.1; - - // Check if the pixel is close to the grid lines - bool isGridLine = ((position.x / size - float(gridPosition.x)) < thicknessFactor) || - ((position.y / size - float(gridPosition.y)) < thicknessFactor); - - return isGridLine ? newColor * currentColor.a : half4(0.0, 0.0, 0.0, 0.0); -} diff --git a/Loop/Window Management/WindowEngine.swift b/Loop/Window Management/WindowEngine.swift index 627f37d0..48bc585c 100644 --- a/Loop/Window Management/WindowEngine.swift +++ b/Loop/Window Management/WindowEngine.swift @@ -122,7 +122,6 @@ enum WindowEngine { print("Failed to get window at cursor: \(error.localizedDescription)") } - if result == nil { do { result = try WindowEngine.getFrontmostWindow() From 7c2d907a4e53b1cbbc8bc6259a0ad7e626c05656 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Fri, 21 Jun 2024 18:47:04 -0600 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=A8=20Ability=20to=20observe=20window?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop.xcodeproj/project.pbxproj | 4 + Loop/Utilities/Observer.swift | 246 ++++++++++++++++++++++++++++ Loop/Window Management/Window.swift | 28 ++++ 3 files changed, 278 insertions(+) create mode 100644 Loop/Utilities/Observer.swift diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 82ead96d..7bbbfbd4 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -49,6 +49,7 @@ A85CB5852ACFA5F700BF63E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85CB5842ACFA5F700BF63E6 /* AppDelegate.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 */; }; + A867C20E2C26522B005831BC /* Observer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A867C20D2C26522B005831BC /* Observer.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 */; }; A86A75102C253BBC004AA154 /* Luminare in Frameworks */ = {isa = PBXBuildFile; productRef = A86A750F2C253BBC004AA154 /* Luminare */; }; @@ -136,6 +137,7 @@ A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.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 = ""; }; + A867C20D2C26522B005831BC /* Observer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observer.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 = ""; }; @@ -220,6 +222,7 @@ A86B97AC2AB79E2500099D7F /* ShakeEffect.swift */, A8D6D3002B6C894C0061B11F /* PaddingModel.swift */, A8D6D3042B6C92F20061B11F /* WallpaperView.swift */, + A867C20D2C26522B005831BC /* Observer.swift */, ); path = Utilities; sourceTree = ""; @@ -566,6 +569,7 @@ A8D4327B2C13ED3C007BE4F2 /* Icon.swift in Sources */, A86B97AD2AB79E2500099D7F /* ShakeEffect.swift in Sources */, A8D6D3032B6C8D750061B11F /* PaddingPreviewView.swift in Sources */, + A867C20E2C26522B005831BC /* Observer.swift in Sources */, A82740982AB00FCE00B9BDC5 /* Color+Extensions.swift in Sources */, A82B1AF62BD35C8500E2F3F9 /* BehaviorConfiguration.swift in Sources */, A869C1A12B38C6E600AD1A84 /* StageManager.swift in Sources */, diff --git a/Loop/Utilities/Observer.swift b/Loop/Utilities/Observer.swift new file mode 100644 index 00000000..149df90e --- /dev/null +++ b/Loop/Utilities/Observer.swift @@ -0,0 +1,246 @@ +// +// Observer.swift +// Loop +// +// Created by Kai Azim on 2024-06-21. +// +// Mostly taken from https://github.com/tmandry/AXSwift/, thank you so much :) + +import Cocoa +import Darwin +import Foundation + +/// Observers watch for events on an application's UI elements. +/// +/// Events are received as part of the application's default run loop. +class Observer { + typealias Callback = ( + _ observer: Observer, + _ window: Window, + _ notification: AXNotification + ) -> () + + typealias CallbackWithInfo = ( + _ observer: Observer, + _ window: Window, + _ notification: AXNotification, + _ info: [String: AnyObject]? + ) -> () + + let pid: pid_t + let axObserver: AXObserver! + let callback: Callback? + let callbackWithInfo: CallbackWithInfo? + +// public fileprivate(set) lazy var application: Application = Application(forKnownProcessID: self.pid)! + + /// Creates and starts an observer on the given `processID`. + public init(processID: pid_t, callback: @escaping Callback) throws { + var axObserver: AXObserver? + let error = AXObserverCreate(processID, internalCallback, &axObserver) + + self.pid = processID + self.axObserver = axObserver + self.callback = callback + self.callbackWithInfo = nil + + guard error == .success else { + throw error + } + assert(axObserver != nil) + + start() + } + + /// Creates and starts an observer on the given `processID`. + /// + /// Use this initializer if you want the extra user info provided with notifications. + public init(processID: pid_t, callback: @escaping CallbackWithInfo) throws { + var axObserver: AXObserver? + let error = AXObserverCreateWithInfoCallback(processID, internalInfoCallback, &axObserver) + + self.pid = processID + self.axObserver = axObserver + self.callback = nil + self.callbackWithInfo = callback + + guard error == .success else { + throw error + } + assert(axObserver != nil) + + start() + } + + deinit { + stop() + } + + /// Starts watching for events. You don't need to call this method unless you use `stop()`. + /// + /// If the observer has already been started, this method does nothing. + public func start() { + CFRunLoopAddSource( + RunLoop.current.getCFRunLoop(), + AXObserverGetRunLoopSource(axObserver), + CFRunLoopMode.defaultMode + ) + } + + /// Stops sending events to your callback until the next call to `start`. + /// + /// If the observer has already been started, this method does nothing. + /// + /// - important: Events will still be queued in the target process until the Observer is started + /// again or destroyed. If you don't want them, create a new Observer. + public func stop() { + CFRunLoopRemoveSource( + RunLoop.current.getCFRunLoop(), + AXObserverGetRunLoopSource(axObserver), + CFRunLoopMode.defaultMode + ) + } + + /// Adds a notification for the observer to watch. + /// + /// - parameter notification: The name of the notification to watch for. + /// - parameter forElement: The element to watch for the notification on. Must belong to the + /// application this observer was created on. + /// - note: The underlying API returns an error if the notification is already added, but that + /// error is not passed on for consistency with `start()` and `stop()`. + /// - throws: `Error.NotificationUnsupported`: The element does not support notifications (note + /// that the system-wide element does not support notifications). + public func addNotification( + _ notification: AXNotification, + forElement element: Window + ) throws { + let selfPtr = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + let error = AXObserverAddNotification( + axObserver, element.axWindow, notification.rawValue as CFString, selfPtr + ) + guard error == .success || error == .notificationAlreadyRegistered else { + throw error + } + } + + /// Removes a notification from the observer. + /// + /// - parameter notification: The name of the notification to stop watching. + /// - parameter forElement: The element to stop watching the notification on. + /// - note: The underlying API returns an error if the notification is not present, but that + /// error is not passed on for consistency with `start()` and `stop()`. + /// - throws: `Error.NotificationUnsupported`: The element does not support notifications (note + /// that the system-wide element does not support notifications). + public func removeNotification( + _ notification: AXNotification, + forElement element: Window + ) throws { + let error = AXObserverRemoveNotification( + axObserver, element.axWindow, notification.rawValue as CFString + ) + guard error == .success || error == .notificationNotRegistered else { + throw error + } + } +} + +private func internalCallback( + _: AXObserver, + axElement: AXUIElement, + notification: CFString, + userData: UnsafeMutableRawPointer? +) { + guard let userData else { fatalError("userData should be an AXSwift.Observer") } + guard let element = try? Window(element: axElement) else { return } + + let observer = Unmanaged.fromOpaque(userData).takeUnretainedValue() + guard let notif = AXNotification(rawValue: notification as String) else { + NSLog("Unknown AX notification %s received", notification as String) + return + } + observer.callback!(observer, element, notif) +} + +private func internalInfoCallback( + _: AXObserver, + axElement: AXUIElement, + notification: CFString, + cfInfo: CFDictionary, + userData: UnsafeMutableRawPointer? +) { + guard let userData else { fatalError("userData should be an AXSwift.Observer") } + guard let element = try? Window(element: axElement) else { return } + + let observer = Unmanaged.fromOpaque(userData).takeUnretainedValue() + let info = cfInfo as NSDictionary? as! [String: AnyObject]? + guard let notif = AXNotification(rawValue: notification as String) else { + NSLog("Unknown AX notification %s received", notification as String) + return + } + observer.callbackWithInfo!(observer, element, notif, info) +} + +public enum AXNotification: String { + // Focus notifications + case mainWindowChanged = "AXMainWindowChanged" + case focusedWindowChanged = "AXFocusedWindowChanged" + case focusedUIElementChanged = "AXFocusedUIElementChanged" + case focusedTabChanged = "AXFocusedTabChanged" + + // Application notifications + case applicationActivated = "AXApplicationActivated" + case applicationDeactivated = "AXApplicationDeactivated" + case applicationHidden = "AXApplicationHidden" + case applicationShown = "AXApplicationShown" + + // Window notifications + case windowCreated = "AXWindowCreated" + case windowMoved = "AXWindowMoved" + case windowResized = "AXWindowResized" + case windowMiniaturized = "AXWindowMiniaturized" + case windowDeminiaturized = "AXWindowDeminiaturized" + + // Drawer & sheet notifications + case drawerCreated = "AXDrawerCreated" + case sheetCreated = "AXSheetCreated" + + // Element notifications + case uiElementDestroyed = "AXUIElementDestroyed" + case valueChanged = "AXValueChanged" + case titleChanged = "AXTitleChanged" + case resized = "AXResized" + case moved = "AXMoved" + case created = "AXCreated" + + // Used when UI changes require the attention of assistive application. Pass along a user info + // dictionary with the key NSAccessibilityUIElementsKey and an array of elements that have been + // added or changed as a result of this layout change. + case layoutChanged = "AXLayoutChanged" + + // Misc notifications + case helpTagCreated = "AXHelpTagCreated" + case selectedTextChanged = "AXSelectedTextChanged" + case rowCountChanged = "AXRowCountChanged" + case selectedChildrenChanged = "AXSelectedChildrenChanged" + case selectedRowsChanged = "AXSelectedRowsChanged" + case selectedColumnsChanged = "AXSelectedColumnsChanged" + case loadComplete = "AXLoadComplete" + + case rowExpanded = "AXRowExpanded" + case rowCollapsed = "AXRowCollapsed" + + // Cell-table notifications + case selectedCellsChanged = "AXSelectedCellsChanged" + + // Layout area notifications + case unitsChanged = "AXUnitsChanged" + case selectedChildrenMoved = "AXSelectedChildrenMoved" + + // This notification allows an application to request that an announcement be made to the user + // by an assistive application such as VoiceOver. The notification requires a user info + // dictionary with the key NSAccessibilityAnnouncementKey and the announcement as a localized + // string. In addition, the key NSAccessibilityAnnouncementPriorityKey should also be used to + // help an assistive application determine the importance of this announcement. This + // notification should be posted for the application element. + case announcementRequested = "AXAnnouncementRequested" +} diff --git a/Loop/Window Management/Window.swift b/Loop/Window Management/Window.swift index db96fa4b..40f558d2 100644 --- a/Loop/Window Management/Window.swift +++ b/Loop/Window Management/Window.swift @@ -27,6 +27,8 @@ class Window { let cgWindowID: CGWindowID let nsRunningApplication: NSRunningApplication? + var observer: Observer? + init(element: AXUIElement) throws { self.axWindow = element @@ -57,6 +59,12 @@ class Window { try self.init(element: window) } + deinit { + if let observer = self.observer { + observer.stop() + } + } + var role: NSAccessibility.Role? { do { guard let value: String = try self.axWindow.getValue(.role) else { @@ -288,4 +296,24 @@ class Window { self.enhancedUserInterface = true } } + + public func createObserver(_ callback: @escaping Observer.Callback) -> Observer? { + do { + return try Observer(processID: try self.axWindow.getPID()!, callback: callback) + } catch AXError.invalidUIElement { + return nil + } catch let error { + fatalError("Caught unexpected error creating observer: \(error)") + } + } + + public func createObserver(_ callback: @escaping Observer.CallbackWithInfo) -> Observer? { + do { + return try Observer(processID: try self.axWindow.getPID()!, callback: callback) + } catch AXError.invalidUIElement { + return nil + } catch let error { + fatalError("Caught unexpected error creating observer: \(error)") + } + } }