Skip to content

Commit

Permalink
Raise windows when interacting with them
Browse files Browse the repository at this point in the history
Fixes #5. (Not fully, of course, but it's a preliminary implementation).
  • Loading branch information
saagarjha committed Mar 2, 2024
1 parent 2a3ab6a commit 5c694c0
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 0 deletions.
7 changes: 7 additions & 0 deletions macOS/Local.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,37 +187,43 @@ 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()
}

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()
}

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()
Expand All @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions macOS/SPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Saagar Jha on 10/21/23.
//

import ApplicationServices
import CoreGraphics

// FB13556001
Expand All @@ -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<CChar>) -> Int64)?.self)

// FB13607820
let _AXUIElementGetWindow = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY), "_AXUIElementGetWindow"), to: (@convention(c) (AXUIElement, UnsafeMutablePointer<CGWindowID>) -> 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<ProcessSerialNumber>) -> OSStatus)?.self)!
let SetFrontProcessWithOptions = unsafeBitCast(dlsym(dlopen(nil, RTLD_LAZY), "SetFrontProcessWithOptions"), to: (@convention(c) (UnsafePointer<ProcessSerialNumber>, OptionBits) -> OSStatus)?.self)!
58 changes: 58 additions & 0 deletions macOS/WindowManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 5c694c0

Please sign in to comment.