Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: use AXUIElement extension #242

Merged
merged 1 commit into from
Aug 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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