Skip to content

Commit

Permalink
refactor: use AXUIElement extension (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShlomoCode authored Aug 5, 2024
1 parent 48b1328 commit b60d8b4
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 163 deletions.
4 changes: 4 additions & 0 deletions DockDoor.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
0531F8D22C3CC04600327808 /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0531F8D12C3CC04600327808 /* AppearanceSettingsView.swift */; };
0596C2C32C3E060D00DCABEF /* PrivateApis.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0596C2C22C3E060D00DCABEF /* PrivateApis.swift */; };
05A25B912C3EF28E002AC594 /* ColorHex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05A25B902C3EF28E002AC594 /* ColorHex.swift */; };
05C0C7182C60629C000ADAC6 /* AXUIElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05C0C7172C60629C000ADAC6 /* AXUIElement.swift */; };
3A105FD62C1BED660015EC66 /* BlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A105FD52C1BED660015EC66 /* BlurView.swift */; };
3A105FD92C1C049E0015EC66 /* dockStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A105FD82C1C049E0015EC66 /* dockStyle.swift */; };
3A105FDD2C1C0EE20015EC66 /* DynStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A105FDC2C1C0EE20015EC66 /* DynStack.swift */; };
Expand Down Expand Up @@ -57,6 +58,7 @@
0531F8D12C3CC04600327808 /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
0596C2C22C3E060D00DCABEF /* PrivateApis.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateApis.swift; sourceTree = "<group>"; };
05A25B902C3EF28E002AC594 /* ColorHex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorHex.swift; sourceTree = "<group>"; };
05C0C7172C60629C000ADAC6 /* AXUIElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AXUIElement.swift; sourceTree = "<group>"; };
3A105FD52C1BED660015EC66 /* BlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurView.swift; sourceTree = "<group>"; };
3A105FD82C1C049E0015EC66 /* dockStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = dockStyle.swift; sourceTree = "<group>"; };
3A105FDC2C1C0EE20015EC66 /* DynStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynStack.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -131,6 +133,7 @@
3A105FD72C1BF11D0015EC66 /* Extensions */ = {
isa = PBXGroup;
children = (
05C0C7172C60629C000ADAC6 /* AXUIElement.swift */,
05A25B902C3EF28E002AC594 /* ColorHex.swift */,
3A105FD82C1C049E0015EC66 /* dockStyle.swift */,
);
Expand Down Expand Up @@ -330,6 +333,7 @@
BBF4139A2C40A64A00AA6733 /* FluidGradient.swift in Sources */,
BB7CA33D2C31F1B00012E303 /* WindowManipulationObservers.swift in Sources */,
BB157B802C0E8E6700997315 /* MessageUtil.swift in Sources */,
05C0C7182C60629C000ADAC6 /* AXUIElement.swift in Sources */,
3A105FD62C1BED660015EC66 /* BlurView.swift in Sources */,
BB1CBD5D2C1BCA4F003969BC /* Misc Utils.swift in Sources */,
05A25B912C3EF28E002AC594 /* ColorHex.swift in Sources */,
Expand Down
118 changes: 118 additions & 0 deletions DockDoor/Extensions/AXUIElement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import Cocoa
import ApplicationServices.HIServices.AXUIElement
import ApplicationServices.HIServices.AXValue
import ApplicationServices.HIServices.AXError
import ApplicationServices.HIServices.AXRoleConstants
import ApplicationServices.HIServices.AXAttributeConstants
import ApplicationServices.HIServices.AXActionConstants

extension AXUIElement {
func axCallWhichCanThrow<T>(_ result: AXError, _ successValue: inout T) throws -> T? {
switch result {
case .success: return successValue
// .cannotComplete can happen if the app is unresponsive; we throw in that case to retry until the call succeeds
case .cannotComplete: throw AxError.runtimeError
// for other errors it's pointless to retry
default: return nil
}
}

func cgWindowId() throws -> CGWindowID? {
var id = CGWindowID(0)
return try axCallWhichCanThrow(_AXUIElementGetWindow(self, &id), &id)
}

func pid() throws -> pid_t? {
var pid = pid_t(0)
return try axCallWhichCanThrow(AXUIElementGetPid(self, &pid), &pid)
}

func attribute<T>(_ key: String, _ _: T.Type) throws -> T? {
var value: AnyObject?
return try axCallWhichCanThrow(AXUIElementCopyAttributeValue(self, key as CFString, &value), &value) as? T
}

private func value<T>(_ key: String, _ target: T, _ type: AXValueType) throws -> T? {
if let a = try attribute(key, AXValue.self) {
var value = target
AXValueGetValue(a, type, &value)
return value
}
return nil
}

func position() throws -> CGPoint? {
return try value(kAXPositionAttribute, CGPoint.zero, .cgPoint)
}

func size() throws -> CGSize? {
return try value(kAXSizeAttribute, CGSize.zero, .cgSize)
}

func title() throws -> String? {
return try attribute(kAXTitleAttribute, String.self)
}

func parent() throws -> AXUIElement? {
return try attribute(kAXParentAttribute, AXUIElement.self)
}

func children() throws -> [AXUIElement]? {
return try attribute(kAXChildrenAttribute, [AXUIElement].self)
}

func windows() throws -> [AXUIElement]? {
return try attribute(kAXWindowsAttribute, [AXUIElement].self)
}

func isMinimized() throws -> Bool {
return try attribute(kAXMinimizedAttribute, Bool.self) == true
}

func isFullscreen() throws -> Bool {
return try attribute(kAXFullscreenAttribute, Bool.self) == true
}

func focusedWindow() throws -> AXUIElement? {
return try attribute(kAXFocusedWindowAttribute, AXUIElement.self)
}

func role() throws -> String? {
return try attribute(kAXRoleAttribute, String.self)
}

func subrole() throws -> String? {
return try attribute(kAXSubroleAttribute, String.self)
}

func appIsRunning() throws -> Bool? {
return try attribute(kAXIsApplicationRunningAttribute, Bool.self)
}

func closeButton() throws -> AXUIElement? {
return try attribute(kAXCloseButtonAttribute, AXUIElement.self)
}

func subscribeToNotification(_ axObserver: AXObserver, _ notification: String, _ callback: (() -> Void)? = nil) throws {
let result = AXObserverAddNotification(axObserver, self, notification as CFString, nil)
if result == .success || result == .notificationAlreadyRegistered {
callback?()
} else if result != .notificationUnsupported && result != .notImplemented {
throw AxError.runtimeError
}
}

func setAttribute(_ key: String, _ value: Any) throws {
var unused: Void = ()
try axCallWhichCanThrow(AXUIElementSetAttributeValue(self, key as CFString, value as CFTypeRef), &unused)
}

func performAction(_ action: String) throws {
var unused: Void = ()
try axCallWhichCanThrow(AXUIElementPerformAction(self, action as CFString), &unused)
}
}

enum AxError: Error {
case runtimeError
}
3 changes: 3 additions & 0 deletions DockDoor/Utilities/PrivateApis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ import Cocoa
// * macOS 10.10+
@_silgen_name("_AXUIElementGetWindow") @discardableResult
func _AXUIElementGetWindow(_ axUiElement: AXUIElement, _ wid: inout CGWindowID) -> AXError

// for some reason, these attributes are missing from AXAttributeConstants
let kAXFullscreenAttribute = "AXFullScreen"
36 changes: 1 addition & 35 deletions DockDoor/Utilities/WindowManipulationObservers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func axObserverCallback(observer: AXObserver, element: AXUIElement, notification
WindowManipulationObservers.debounceWorkItem?.cancel()
WindowManipulationObservers.debounceWorkItem = DispatchWorkItem {
WindowUtil.updateWindowCache(for: app.bundleIdentifier ?? "") { windowSet in
windowSet = windowSet.filter { WindowUtil.isElementValid($0.axElement) }
windowSet = windowSet.filter { WindowUtil.isValidElement($0.axElement) }
}
WindowManipulationObservers.trackedElements.remove(element)
print("Window destroyed for app: \(app.localizedName ?? "Unknown")")
Expand All @@ -163,37 +163,3 @@ func axObserverCallback(observer: AXObserver, element: AXUIElement, notification
}
}
}

func printTitle(of element: AXUIElement) {
var title: AnyObject?
let result = AXUIElementCopyAttributeValue(element, kAXTitleAttribute as CFString, &title)

if result == .success, let titleString = title as? String {
print("Title: \(titleString)")
} else {
print("Unable to retrieve title")
}
}

enum AxError: Error {
case runtimeError
}

func axCallWhichCanThrow<T>(_ result: AXError, _ successValue: inout T) throws -> T? {
switch result {
case .success:
print(successValue)
return successValue
case .cannotComplete:
print("error")
throw AxError.runtimeError
default:
return nil
}
}

func cgWindowId(appElement: AXUIElement) throws -> CGWindowID? {
var id: CGWindowID = 0
let result = _AXUIElementGetWindow(appElement, &id)
return try axCallWhichCanThrow(result, &id)
}
Loading

0 comments on commit b60d8b4

Please sign in to comment.