Skip to content

Commit

Permalink
weak self referencing to prevent abandoned memory allocation
Browse files Browse the repository at this point in the history
also fixes forced unwrapped of optional bundle ID value
  • Loading branch information
ejbills committed Jun 26, 2024
1 parent b37b389 commit e21baf9
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 64 deletions.
Binary file not shown.
23 changes: 16 additions & 7 deletions DockDoor/Utilities/App Icon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,41 @@ import AppKit
struct AppIconUtil {
// MARK: - Properties

private static var iconCache: [String: NSImage] = [:]
private static var iconCache: [String: (image: NSImage, timestamp: Date)] = [:]
private static let cacheExpiryInterval: TimeInterval = 3600 // 1 hour

// MARK: - App Icons

static func getIcon(file path: URL) -> NSImage? {
let cacheKey = path.path
removeExpiredCacheEntries()

if let cachedIcon = iconCache[cacheKey] {
return cachedIcon
if let cachedEntry = iconCache[cacheKey], Date().timeIntervalSince(cachedEntry.timestamp) < cacheExpiryInterval {
return cachedEntry.image
}

guard FileManager.default.fileExists(atPath: path.path) else {
return nil
}

let icon = NSWorkspace.shared.icon(forFile: path.path)
iconCache[cacheKey] = icon
iconCache[cacheKey] = (image: icon, timestamp: Date())
return icon
}

static func getIcon(bundleID: String) -> NSImage? {
if let cachedIcon = iconCache[bundleID] {
return cachedIcon
removeExpiredCacheEntries()

if let cachedEntry = iconCache[bundleID], Date().timeIntervalSince(cachedEntry.timestamp) < cacheExpiryInterval {
return cachedEntry.image
}

guard let path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else {
return nil
}

let icon = getIcon(file: path)
iconCache[bundleID] = icon
iconCache[bundleID] = (image: icon!, timestamp: Date())
return icon
}

Expand All @@ -63,4 +67,9 @@ struct AppIconUtil {
static func clearCache() {
iconCache.removeAll()
}

private static func removeExpiredCacheEntries() {
let now = Date()
iconCache = iconCache.filter { now.timeIntervalSince($0.value.timestamp) < cacheExpiryInterval }
}
}
45 changes: 22 additions & 23 deletions DockDoor/Utilities/DockObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@
import Cocoa
import ApplicationServices

class DockObserver {
final class DockObserver {
static let shared = DockObserver()

private var lastAppName: String?
private var lastMouseLocation: CGPoint?
private let mouseUpdateThreshold: CGFloat = 5.0
private var eventTap: CFMachPort?

private var hoverProcessingTask: DispatchWorkItem?
private var hoverProcessingTask: Task<Void, Error>?
private var isProcessing: Bool = false

private var dockAppProcessIdentifier: pid_t? = nil
Expand Down Expand Up @@ -50,13 +50,13 @@ class DockObserver {

func eventTapCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let observer = Unmanaged<DockObserver>.fromOpaque(refcon!).takeUnretainedValue()

if type == .mouseMoved {
let mouseLocation = event.location
observer.handleMouseEvent(mouseLocation: mouseLocation)
}
return Unmanaged.passRetained(event)

return Unmanaged.passUnretained(event)
}

eventTap = CGEvent.tapCreate(
Expand All @@ -77,33 +77,32 @@ class DockObserver {
}
}

@objc private func handleMouseEvent(mouseLocation: CGPoint) {
// Cancel any ongoing processing
private func handleMouseEvent(mouseLocation: CGPoint) {
hoverProcessingTask?.cancel()

// Create a new task for hover processing
hoverProcessingTask = DispatchWorkItem { [weak self] in
self?.processMouseEvent(mouseLocation: mouseLocation)
hoverProcessingTask = Task { [weak self] in
guard let self = self else { return }
try Task.checkCancellation()
self.processMouseEvent(mouseLocation: mouseLocation)
}

// Execute the new task
DispatchQueue.main.async(execute: hoverProcessingTask!)
}

private func processMouseEvent(mouseLocation: CGPoint) {
guard !isProcessing, !CurrentWindow.shared.showingTabMenu else { return }
isProcessing = true

defer {
isProcessing = false
}

guard let lastMouseLocation = lastMouseLocation else {
self.lastMouseLocation = mouseLocation
isProcessing = false
return
}

// Ignore minor movements
if abs(mouseLocation.x - lastMouseLocation.x) < mouseUpdateThreshold &&
abs(mouseLocation.y - lastMouseLocation.y) < mouseUpdateThreshold {
isProcessing = false
return
}
self.lastMouseLocation = mouseLocation
Expand All @@ -115,7 +114,8 @@ class DockObserver {
if dockIconAppName != lastAppName {
lastAppName = dockIconAppName

Task {
Task { [weak self] in
guard let self = self else { return }
do {
let activeWindows = try await WindowUtil.activeWindows(for: dockIconAppName)
await MainActor.run {
Expand All @@ -130,30 +130,29 @@ class DockObserver {
windows: activeWindows,
mouseLocation: convertedMouseLocation,
mouseScreen: mouseScreen,
onWindowTap: { HoverWindow.shared.hideWindow() }
onWindowTap: { [weak self] in
HoverWindow.shared.hideWindow()
self?.lastAppName = nil
}
)
}
self.isProcessing = false
}
} catch {
await MainActor.run {
print("Error fetching active windows: \(error)")
self.isProcessing = false
}
}
}
} else {
isProcessing = false
}
} else {
Task { @MainActor in
Task { @MainActor [weak self] in
guard let self = self else { return }
let mouseScreen = DockObserver.screenContainingPoint(currentMouseLocation) ?? NSScreen.main!
let convertedMouseLocation = DockObserver.nsPointFromCGPoint(currentMouseLocation, forScreen: mouseScreen)
if !HoverWindow.shared.frame.contains(convertedMouseLocation) {
self.lastAppName = nil
HoverWindow.shared.hideWindow()
}
self.isProcessing = false
}
}
}
Expand Down
8 changes: 5 additions & 3 deletions DockDoor/Utilities/KeybindHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,13 @@ class KeybindHelper {
}

private func showHoverWindow() {
Task {
Task { [weak self] in
do {
guard let self = self else { return }
let windows = try await WindowUtil.activeWindows(for: "")
await MainActor.run {
if isControlKeyPressed { // Check if Ctrl key is still pressed
await MainActor.run { [weak self] in
guard let self = self else { return }
if self.isControlKeyPressed {
HoverWindow.shared.showWindow(appName: "Alt-Tab", windows: windows, overrideDelay: true, onWindowTap: { HoverWindow.shared.hideWindow() })
}
}
Expand Down
72 changes: 51 additions & 21 deletions DockDoor/Utilities/WindowUtil.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ struct CachedAppIcon {
let timestamp: Date
}

struct WindowUtil {
final class WindowUtil {
private static var imageCache: [CGWindowID: CachedImage] = [:]
private static var iconCache: [String: CachedAppIcon] = [:]
private static let cacheQueue = DispatchQueue(label: "com.dockdoor.cacheQueue", attributes: .concurrent)
private static var cacheExpirySeconds: Double = 600 // 10 mins
private static var cacheExpirySeconds: Double = 60 // 1 min

// MARK: - Cache Management

Expand All @@ -47,14 +46,14 @@ struct WindowUtil {
let now = Date()
cacheQueue.async(flags: .barrier) {
imageCache = imageCache.filter { now.timeIntervalSince($0.value.timestamp) <= cacheExpirySeconds }
iconCache = iconCache.filter { now.timeIntervalSince($0.value.timestamp) <= cacheExpirySeconds }
}
}

/// Resets the image and icon cache.
static func resetCache() {
imageCache.removeAll()
iconCache.removeAll()
cacheQueue.async(flags: .barrier) {
imageCache.removeAll()
}
}

// MARK: - Helper Functions
Expand Down Expand Up @@ -300,8 +299,8 @@ struct WindowUtil {
for windowInfo in minimizedWindowsInfo {
await group.addTask { return windowInfo }
}
} else if !applicationName.isEmpty, let app = getRunningApplication(named: applicationName) {
let minimizedWindowsInfo = getMinimizedWindows(pid: app.processIdentifier, bundleID: app.bundleIdentifier!, appName: applicationName)
} else if !applicationName.isEmpty, let app = getRunningApplication(named: applicationName), let bundleID = app.bundleIdentifier {
let minimizedWindowsInfo = getMinimizedWindows(pid: app.processIdentifier, bundleID: bundleID, appName: applicationName)
for windowInfo in minimizedWindowsInfo {
await group.addTask { return windowInfo }
}
Expand Down Expand Up @@ -369,34 +368,65 @@ actor LimitedTaskGroup<T> {
private var tasks: [Task<T, Error>] = []
private let maxConcurrentTasks: Int
private var runningTasks = 0
private let semaphore: AsyncSemaphore

init(maxConcurrentTasks: Int) {
self.maxConcurrentTasks = maxConcurrentTasks
self.semaphore = AsyncSemaphore(value: maxConcurrentTasks)
}

func addTask(_ operation: @escaping () async throws -> T) {
let task = Task {
while self.runningTasks >= self.maxConcurrentTasks {
try await Task.sleep(nanoseconds: 10_000_000) // 10ms
}
self.runningTasks += 1
defer { self.runningTasks -= 1 }
await semaphore.wait()
defer { Task { await semaphore.signal() } }
return try await operation()
}
tasks.append(task)
}

func waitForAll() async throws -> [T] {
var results: [T] = []
for task in tasks {
do {
let result = try await task.value
defer { tasks.removeAll() }

return try await withThrowingTaskGroup(of: T.self) { group in
for task in tasks {
group.addTask {
try await task.value
}
}

var results: [T] = []
for try await result in group {
results.append(result)
} catch {
print("Task failed with error: \(error)")
}
return results
}
}
}

actor AsyncSemaphore {
private var value: Int
private var waiters: [CheckedContinuation<Void, Never>] = []

init(value: Int) {
self.value = value
}

func wait() async {
if value > 0 {
value -= 1
} else {
await withCheckedContinuation { continuation in
waiters.append(continuation)
}
}
}

func signal() {
if let waiter = waiters.first {
waiters.removeFirst()
waiter.resume()
} else {
value += 1
}
tasks.removeAll()
return results
}
}
18 changes: 8 additions & 10 deletions DockDoor/Views/HoverWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import Defaults
}
}

class HoverWindow: NSWindow {
final class HoverWindow: NSWindow {
static let shared = HoverWindow()

private var appName: String = ""
Expand Down Expand Up @@ -62,8 +62,6 @@ class HoverWindow: NSWindow {
}

func hideWindow() {
guard self.hostingView != nil else { return }

DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.contentView = nil
Expand All @@ -77,7 +75,7 @@ class HoverWindow: NSWindow {
}

private func updateContentViewSizeAndPosition(mouseLocation: CGPoint? = nil, mouseScreen: NSScreen, animated: Bool, centerOnScreen: Bool = false) {
guard let hostingView else { return }
guard let hostingView = hostingView else { return }

if centerOnScreen {
CurrentWindow.shared.setShowing(toState: true)
Expand Down Expand Up @@ -135,8 +133,8 @@ class HoverWindow: NSWindow {
}

// Ensure the hover window stays within the dock screen bounds
xPosition = max(screenFrame.minX, min(xPosition, screenFrame.maxX - newHoverWindowSize.width)) + Defaults[.windowPadding]
yPosition = max(screenFrame.minY, min(yPosition, screenFrame.maxY - newHoverWindowSize.height))
xPosition = max(screenFrame.minX, min(xPosition, screenFrame.maxX - newHoverWindowSize.width)) + (dockPosition != .bottom ? Defaults[.windowPadding] : 0)
yPosition = max(screenFrame.minY, min(yPosition, screenFrame.maxY - newHoverWindowSize.height)) + (dockPosition == .bottom ? Defaults[.windowPadding] : 0)

position = CGPoint(x: xPosition, y: yPosition)

Expand Down Expand Up @@ -207,13 +205,13 @@ class HoverWindow: NSWindow {

DispatchQueue.main.async { [weak self] in
guard let self = self else { return }

self.appName = appName
self.windows = windows
self.onWindowTap = onWindowTap

let screen = mouseScreen ?? NSScreen.main!

if self.hostingView == nil {
let hoverView = HoverView(appName: appName, windows: windows, onWindowTap: onWindowTap,
dockPosition: DockUtils.shared.getDockPosition(), bestGuessMonitor: screen)
Expand All @@ -224,7 +222,7 @@ class HoverWindow: NSWindow {
self.hostingView?.rootView = HoverView(appName: appName, windows: windows, onWindowTap: onWindowTap,
dockPosition: DockUtils.shared.getDockPosition(), bestGuessMonitor: screen)
}

self.updateContentViewSizeAndPosition(mouseLocation: mouseLocation, mouseScreen: screen, animated: true, centerOnScreen: !isMouseEvent)
self.makeKeyAndOrderFront(nil)
}
Expand Down

0 comments on commit e21baf9

Please sign in to comment.