diff --git a/macOS/Local.swift b/macOS/Local.swift index ecac35c..3723cc2 100644 --- a/macOS/Local.swift +++ b/macOS/Local.swift @@ -187,23 +187,27 @@ class Local: LocalInterface, macOSInterface { func _clicked(parameters: M.Clicked.Request) async throws -> M.Clicked.Reply { let window = try await windowManager.lookupWindow(byID: parameters.windowID)! + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectClick(at: .init(x: window.frame.minX + window.frame.width * parameters.x, y: window.frame.minY + window.frame.height * parameters.y)) return .init() } func _scrollBegan(parameters: M.ScrollBegan.Request) async throws -> M.ScrollBegan.Reply { + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectScrollBegan() return .init() } func _scrollChanged(parameters: M.ScrollChanged.Request) async throws -> M.ScrollChanged.Reply { + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectScrollChanged(translationX: parameters.x, translationY: parameters.y) return .init() } func _scrollEnded(parameters: M.ScrollEnded.Request) async throws -> M.ScrollEnded.Reply { + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectScrollEnded() return .init() @@ -211,6 +215,7 @@ class Local: LocalInterface, macOSInterface { func _dragBegan(parameters: M.DragBegan.Request) async throws -> M.DragBegan.Reply { let window = try await windowManager.lookupWindow(byID: parameters.windowID)! + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectDragBegan(at: .init(x: window.frame.minX + window.frame.width * parameters.x, y: window.frame.minY + window.frame.height * parameters.y)) return .init() @@ -218,6 +223,7 @@ class Local: LocalInterface, macOSInterface { func _dragChanged(parameters: M.DragChanged.Request) async throws -> M.DragChanged.Reply { let window = try await windowManager.lookupWindow(byID: parameters.windowID)! + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectDragChanged(to: .init(x: window.frame.minX + window.frame.width * parameters.x, y: window.frame.minY + window.frame.height * parameters.y)) return .init() @@ -231,6 +237,7 @@ class Local: LocalInterface, macOSInterface { } func _typed(parameters: M.Typed.Request) async throws -> M.Typed.Reply { + await windowManager.activateWindow(identifiedBy: parameters.windowID) await eventDispatcher.injectKey(key: parameters.key, down: parameters.down) return .init() diff --git a/macOS/SPI.swift b/macOS/SPI.swift index 99fa5cd..7582e0c 100644 --- a/macOS/SPI.swift +++ b/macOS/SPI.swift @@ -5,6 +5,7 @@ // Created by Saagar Jha on 10/21/23. // +import ApplicationServices import CoreGraphics // FB13556001 @@ -15,3 +16,10 @@ let SLSCopyAssociatedWindows = unsafeBitCast(dlsym(skylight, "SLSCopyAssociatedW // FB13607817 let sandbox_extension_consume = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY | RTLD_NOLOAD), "sandbox_extension_consume"), to: (@convention(c) (UnsafePointer) -> Int64)?.self) + +// FB13607820 +let _AXUIElementGetWindow = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY), "_AXUIElementGetWindow"), to: (@convention(c) (AXUIElement, UnsafeMutablePointer) -> AXError)?.self) + +// This isn't even SPI, it's just deprecated API that Swift refuses to expose +let GetProcessForPID = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY), "GetProcessForPID"), to: (@convention(c) (pid_t, UnsafePointer) -> OSStatus)?.self)! +let SetFrontProcessWithOptions = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY), "SetFrontProcessWithOptions"), to: (@convention(c) (UnsafePointer, OptionBits) -> OSStatus)?.self)! diff --git a/macOS/WindowManager.swift b/macOS/WindowManager.swift index 0cb2a2d..89e2ccc 100644 --- a/macOS/WindowManager.swift +++ b/macOS/WindowManager.swift @@ -135,6 +135,64 @@ actor WindowManager { return application.childWindows(of: window) } } + + func activatedWindow() -> CGWindowID? { + let app = NSWorkspace.shared.frontmostApplication! + let element = AXUIElementCreateApplication(app.processIdentifier) + + var window: CFTypeRef? + AXUIElementCopyAttributeValue(element, kAXMainWindowAttribute as CFString, &window) + return (window as! AXUIElement?)?.windowID + } + + func activateWindow(identifiedBy windowID: CGWindowID) async { + guard _AXUIElementGetWindow != nil else { + return + } + + guard activatedWindow() != windowID else { + return + } + + let window = windows[windowID]! + let application = window.application! + + var psn = ProcessSerialNumber() + _ = GetProcessForPID(application.application.processID, &psn) + _ = SetFrontProcessWithOptions(&psn, OptionBits(kSetFrontProcessFrontWindowOnly | kSetFrontProcessCausedByUser)) + + var windows: CFArray? + AXUIElementCopyAttributeValues(AXUIElementCreateApplication(application.application.processID), kAXWindowsAttribute as CFString, 0, .max, &windows) + guard + let element = (windows as? [AXUIElement] ?? []).first(where: { + $0.windowID == windowID + }) + else { + return + } + + AXUIElementPerformAction(element, kAXRaiseAction as CFString) + + // TODO: Don't poll for this + while activatedWindow() != windowID { + try? await Task.sleep(for: .milliseconds(100)) + } + } +} + +extension AXUIElement { + var windowID: CGWindowID { + assert( + { + var role: CFTypeRef? + AXUIElementCopyAttributeValue(self, kAXRoleAttribute as CFString, &role) + return role as! String == kAXWindowRole + }()) + + var windowID: CGWindowID = 0 + _ = _AXUIElementGetWindow!(self, &windowID) + return windowID + } } extension AXObserver {