Skip to content

Commit

Permalink
feat: use dock item AX element to place hover preview
Browse files Browse the repository at this point in the history
closes #277
  • Loading branch information
ejbills committed Sep 13, 2024
1 parent f272f9a commit d861f8e
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 217 deletions.
195 changes: 110 additions & 85 deletions DockDoor/Utilities/DockObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct ApplicationReturnType {
}

let status: Status
let iconRect: CGRect?
}

func handleSelectedDockItemChangedNotification(observer _: AXObserver, element _: AXUIElement, notificationName _: CFString, _: UnsafeMutableRawPointer?) {
Expand Down Expand Up @@ -84,76 +85,86 @@ final class DockObserver {
hoverProcessingTask?.cancel()
hoverProcessingTask = Task { [weak self] in
guard let self else { return }
try Task.checkCancellation()
guard !isProcessing, await !SharedPreviewWindowCoordinator.shared.windowSwitcherCoordinator.windowSwitcherActive else { return }
isProcessing = true
do {
try Task.checkCancellation()
guard !isProcessing,
await !SharedPreviewWindowCoordinator.shared.windowSwitcherCoordinator.windowSwitcherActive else { return }
isProcessing = true
defer {
self.isProcessing = false
}

defer {
self.isProcessing = false
}
let currentMouseLocation = DockObserver.getMousePosition()
let appUnderMouseElement = getDockItemAppStatusUnderMouse()

let currentMouseLocation = DockObserver.getMousePosition()
let appReturnType = getDockItemAppStatusUnderMouse()

switch appReturnType.status {
case let .success(currentAppUnderMouse):
let currentAppInfo = ApplicationInfo(
processIdentifier: currentAppUnderMouse.processIdentifier,
bundleIdentifier: currentAppUnderMouse.bundleIdentifier,
localizedName: currentAppUnderMouse.localizedName
)

if currentAppInfo.processIdentifier != lastAppUnderMouse?.processIdentifier {
lastAppUnderMouse = currentAppInfo

Task { [weak self] in
guard let self else { return }
do {
guard let app = currentAppInfo.app() else {
print("Failed to get NSRunningApplication for pid: \(currentAppInfo.processIdentifier)")
return
}
switch appUnderMouseElement.status {
case let .success(currentAppUnderMouse):
guard let iconRect = appUnderMouseElement.iconRect else {
print("Icon rect is nil, cannot show window")
return
}

let appWindows = try await WindowUtil.getActiveWindows(of: app)

await MainActor.run {
if appWindows.isEmpty {
self.hideWindowAndResetLastApp()
} else {
let mouseScreen = NSScreen.screenContainingMouse(currentMouseLocation)
let convertedMouseLocation = DockObserver.nsPointFromCGPoint(currentMouseLocation, forScreen: mouseScreen)

SharedPreviewWindowCoordinator.shared.showWindow(
appName: currentAppInfo.localizedName ?? "Unknown",
windows: appWindows,
mouseLocation: convertedMouseLocation,
mouseScreen: mouseScreen,
onWindowTap: { [weak self] in
self?.hideWindowAndResetLastApp()
}
)
let currentAppInfo = ApplicationInfo(
processIdentifier: currentAppUnderMouse.processIdentifier,
bundleIdentifier: currentAppUnderMouse.bundleIdentifier,
localizedName: currentAppUnderMouse.localizedName
)

if currentAppInfo.processIdentifier != lastAppUnderMouse?.processIdentifier {
lastAppUnderMouse = currentAppInfo

Task { [weak self] in
guard let self else { return }
do {
guard let app = currentAppInfo.app() else {
print("Failed to get NSRunningApplication for pid: \(currentAppInfo.processIdentifier)")
return
}

let appWindows = try await WindowUtil.getActiveWindows(of: app)

await MainActor.run {
if appWindows.isEmpty {
self.hideWindowAndResetLastApp()
} else {
let mouseScreen = NSScreen.screenContainingMouse(currentMouseLocation)
let convertedMouseLocation = DockObserver.nsPointFromCGPoint(currentMouseLocation, forScreen: mouseScreen)

SharedPreviewWindowCoordinator.shared.showWindow(
appName: currentAppInfo.localizedName ?? "Unknown",
windows: appWindows,
mouseLocation: convertedMouseLocation,
mouseScreen: mouseScreen,
iconRect: iconRect,
onWindowTap: { [weak self] in
self?.hideWindowAndResetLastApp()
}
)
}
}
} catch {
await MainActor.run {
print("Error fetching active windows: \(error)")
}
}
} catch {
await MainActor.run {
print("Error fetching active windows: \(error)")
}
}
}
}
previousStatus = .success(currentAppUnderMouse)
previousStatus = .success(currentAppUnderMouse)

case let .notRunning(bundleIdentifier):
if case .notRunning = previousStatus {
hideWindowAndResetLastApp()
}
previousStatus = .notRunning(bundleIdentifier: bundleIdentifier)
case let .notRunning(bundleIdentifier):
if case .notRunning = previousStatus {
hideWindowAndResetLastApp()
}
previousStatus = .notRunning(bundleIdentifier: bundleIdentifier)

case .notFound:
if await !SharedPreviewWindowCoordinator.shared.frame.contains(currentMouseLocation) {
hideWindowAndResetLastApp()
case .notFound:
if await !SharedPreviewWindowCoordinator.shared.frame.contains(currentMouseLocation) {
hideWindowAndResetLastApp()
}
previousStatus = .notFound
}
previousStatus = .notFound
} catch {
print("Task cancelled or error occurred: \(error)")
}
}
}
Expand Down Expand Up @@ -189,42 +200,56 @@ final class DockObserver {

func getDockItemAppStatusUnderMouse() -> ApplicationReturnType {
guard let hoveredDockItem = getHoveredApplicationDockItem() else {
return ApplicationReturnType(status: .notFound)
return ApplicationReturnType(status: .notFound, iconRect: nil)
}

var appURL: CFTypeRef?
guard AXUIElementCopyAttributeValue(hoveredDockItem, kAXURLAttribute as CFString, &appURL) == .success, let appURL = appURL as? NSURL as? URL else {
print("Failed to get app URL or convert NSURL to URL")
return ApplicationReturnType(status: .notFound)
}
let iconRect = getIconRect(for: hoveredDockItem)

let bundle = Bundle(url: appURL)
guard let bundleIdentifier = bundle?.bundleIdentifier else {
print("App has no valid bundle. app url: \(appURL.path)") // For example: scrcpy, Android studio emulator
do {
guard let appURL = try hoveredDockItem.attribute(kAXURLAttribute, NSURL.self)?.absoluteURL else {
throw AxError.runtimeError
}

let bundle = Bundle(url: appURL)
guard let bundleIdentifier = bundle?.bundleIdentifier else {
print("App has no valid bundle. App URL: \(appURL.path)")

// MARK: fallback method
// Fallback method
guard let dockItemTitle = try hoveredDockItem.title() else {
print("Failed to get dock item title")
return ApplicationReturnType(status: .notFound, iconRect: iconRect)
}

var dockItemTitle: CFTypeRef?
guard AXUIElementCopyAttributeValue(hoveredDockItem, kAXTitleAttribute as CFString, &dockItemTitle) == .success, let dockItemTitle = dockItemTitle as? String else {
print("Failed to get dock item title")
return ApplicationReturnType(status: .notFound)
if let app = WindowUtil.findRunningApplicationByName(named: dockItemTitle) {
return ApplicationReturnType(status: .success(app), iconRect: iconRect)
} else {
print("Did not find app by name for dock item: \(dockItemTitle)")
return ApplicationReturnType(status: .notFound, iconRect: iconRect)
}
}

if let app = WindowUtil.findRunningApplicationByName(named: dockItemTitle) {
print("Found app by name for dock item: \(dockItemTitle)")
return ApplicationReturnType(status: .success(app))
if let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first {
return ApplicationReturnType(status: .success(runningApp), iconRect: iconRect)
} else {
print("Not Found app by name for dock item: \(dockItemTitle)")
return ApplicationReturnType(status: .notFound)
return ApplicationReturnType(status: .notRunning(bundleIdentifier: bundleIdentifier), iconRect: iconRect)
}
} catch {
print("Error getting application status: \(error)")
return ApplicationReturnType(status: .notFound, iconRect: iconRect)
}
}

if let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first {
print("Current App is running (\(runningApp.localizedName ?? "Unknown"))")
return ApplicationReturnType(status: .success(runningApp))
} else {
print("Current App is not running (\(bundleIdentifier))")
return ApplicationReturnType(status: .notRunning(bundleIdentifier: bundleIdentifier))
private func getIconRect(for dockItem: AXUIElement) -> CGRect? {
do {
guard let position = try dockItem.position(),
let size = try dockItem.size()
else {
return nil
}
return CGRect(origin: position, size: size)
} catch {
print("Error getting icon rect: \(error)")
return nil
}
}

Expand Down
107 changes: 9 additions & 98 deletions DockDoor/Utilities/DockUtils.swift
Original file line number Diff line number Diff line change
@@ -1,112 +1,23 @@
import Cocoa

enum DockPosition {
case top
case bottom
case left
case right
case unknown
}

class DockUtils {
static let shared = DockUtils()

private let dockDefaults: UserDefaults? // Store a single instance

private init() {
dockDefaults = UserDefaults(suiteName: "com.apple.dock")
}

func isDockHidingEnabled() -> Bool {
if let dockAutohide = dockDefaults?.bool(forKey: "autohide") {
return dockAutohide
}

return false
}

func countIcons() -> (Int, Int) {
let persistentAppsCount = dockDefaults?.array(forKey: "persistent-apps")?.count ?? 0
let recentAppsCount = dockDefaults?.array(forKey: "recent-apps")?.count ?? 0
return (persistentAppsCount + recentAppsCount, (persistentAppsCount > 0 && recentAppsCount > 0) ? 1 : 0)
}

func calculateDockWidth() -> CGFloat {
let countIcons = countIcons()
let iconCount = countIcons.0
let numberOfDividers = countIcons.1
let tileSize = tileSize()

let baseWidth = tileSize * CGFloat(iconCount)
let dividerWidth: CGFloat = 10.0
let totalDividerWidth = CGFloat(numberOfDividers) * dividerWidth

if isMagnificationEnabled(),
let largeSize = dockDefaults?.object(forKey: "largesize") as? CGFloat
{
let extraWidth = (largeSize - tileSize) * CGFloat(iconCount) * 0.5
return baseWidth + extraWidth + totalDividerWidth
}

return baseWidth + totalDividerWidth
}

private func tileSize() -> CGFloat {
dockDefaults?.double(forKey: "tilesize") ?? 0
}

private func largeSize() -> CGFloat {
dockDefaults?.double(forKey: "largesize") ?? 0
}

func isMagnificationEnabled() -> Bool {
dockDefaults?.bool(forKey: "magnification") ?? false
}

func calculateDockHeight(_ forScreen: NSScreen?) -> CGFloat {
if isDockHidingEnabled() {
return abs(largeSize() - tileSize())
} else {
if let currentScreen = forScreen {
switch getDockPosition() {
case .right, .left:
let size = abs(currentScreen.frame.width - currentScreen.visibleFrame.width)
return size
case .bottom:
let size = currentScreen.frame.height - currentScreen.visibleFrame.height - getStatusBarHeight(screen: currentScreen) - 1
return size
default:
break
}
}
return 0.0
}
}

func getStatusBarHeight(screen: NSScreen?) -> CGFloat {
var statusBarHeight = 0.0
if let screen {
statusBarHeight = screen.frame.height - screen.visibleFrame.height - (screen.visibleFrame.origin.y - screen.frame.origin.y) - 1
}
return statusBarHeight
}

func getDockPosition() -> DockPosition {
guard let orientation = dockDefaults?.string(forKey: "orientation")?.lowercased() else {
if NSScreen.main!.visibleFrame.origin.y == 0, !isDockHidingEnabled() {
if NSScreen.main!.visibleFrame.origin.x == 0 {
return .right
} else {
return .left
}
} else {
return .bottom
}
}

static func getDockPosition() -> DockPosition {
var orientation: Int32 = 0
var pinning: Int32 = 0
CoreDockGetOrientationAndPinning(&orientation, &pinning)
switch orientation {
case "left": return .left
case "bottom": return .bottom
case "right": return .right
case 1: return .top
case 2: return .bottom
case 3: return .left
case 4: return .right
default: return .unknown
}
}
Expand Down
3 changes: 2 additions & 1 deletion DockDoor/Utilities/KeybindHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ class KeybindHelper {
let windows = WindowUtil.getAllWindowsOfAllApps()

SharedPreviewWindowCoordinator.shared.showWindow(
appName: "Alt-Tab",
appName: "Window Switcher",
windows: windows,
iconRect: nil,
overrideDelay: true,
centeredHoverWindowState: .windowSwitcher,
onWindowTap: { SharedPreviewWindowCoordinator.shared.hideWindow() }
Expand Down
4 changes: 4 additions & 0 deletions DockDoor/Utilities/PrivateApis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ func _AXUIElementGetWindow(_ axUiElement: AXUIElement, _ wid: inout CGWindowID)

// for some reason, these attributes are missing from AXAttributeConstants
let kAXFullscreenAttribute = "AXFullScreen"

// returns CoreDock orientation and pinning state
@_silgen_name("CoreDockGetOrientationAndPinning")
func CoreDockGetOrientationAndPinning(_ outOrientation: UnsafeMutablePointer<Int32>, _ outPinning: UnsafeMutablePointer<Int32>)
Loading

0 comments on commit d861f8e

Please sign in to comment.