fix: quitting apps was not properly removing apps from the list
Instead of using the PSN, we now use .equals() to compare 2 NSRunningApplication as recommended by Apple docs
This commit is contained in:
1 parent
bfc27008cc
commit
10b2c714ca
7 files changed
+56
-63
No files matched your search
@@ -72,3 +72,16 @@ extension NSObject {
|
||||
removeObserver(observer, forKeyPath: key)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Window {
|
||||
func firstIndexThatMatches(_ element: AXUIElement) -> Self.Index? {
|
||||
// `CFEqual` is safer than comparing `CGWindowID` because it will succeed even if the window is deallocated
|
||||
// by the OS, in which case the `CGWindowID` will be `-1`
|
||||
return firstIndex(where: { CFEqual($0.axUiElement, element) })
|
||||
}
|
||||
|
||||
func firstWindowThatMatches(_ element: AXUIElement) -> Window? {
|
||||
guard let index = firstIndexThatMatches(element) else { return nil }
|
||||
return self[index]
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class Application: NSObject {
|
||||
func observeNewWindows() {
|
||||
var newWindows = [AXUIElement]()
|
||||
for window in getActualWindows() {
|
||||
guard Windows.listRecentlyUsedFirst.firstIndexThatMatches(window) == nil else { continue }
|
||||
guard Windows.list.firstIndexThatMatches(window) == nil else { continue }
|
||||
newWindows.append(window)
|
||||
}
|
||||
addWindows(newWindows)
|
||||
@@ -53,7 +53,7 @@ class Application: NSObject {
|
||||
}
|
||||
|
||||
private func addWindows(_ windows: [AXUIElement]) {
|
||||
Windows.listRecentlyUsedFirst.insert(contentsOf: windows.map { Window($0, self) }, at: 0)
|
||||
Windows.list.insert(contentsOf: windows.map { Window($0, self) }, at: 0)
|
||||
}
|
||||
|
||||
private func observeEvents(_ windows: [AXUIElement]) {
|
||||
@@ -77,10 +77,10 @@ func axObserverApplicationCallback(observer: AXObserver, element: AXUIElement, n
|
||||
case kAXApplicationActivatedNotification:
|
||||
guard !app.appIsBeingUsed,
|
||||
let appFocusedWindow = element.focusedWindow(),
|
||||
let existingIndex = Windows.listRecentlyUsedFirst.firstIndexThatMatches(appFocusedWindow) else { return }
|
||||
Windows.listRecentlyUsedFirst.insert(Windows.listRecentlyUsedFirst.remove(at: existingIndex), at: 0)
|
||||
let existingIndex = Windows.list.firstIndexThatMatches(appFocusedWindow) else { return }
|
||||
Windows.list.insert(Windows.list.remove(at: existingIndex), at: 0)
|
||||
case kAXApplicationHiddenNotification, kAXApplicationShownNotification:
|
||||
for window in Windows.listRecentlyUsedFirst {
|
||||
for window in Windows.list {
|
||||
guard window.application.axUiElement!.pid() == element.pid() else { continue }
|
||||
window.isHidden = type == kAXApplicationHiddenNotification
|
||||
}
|
||||
@@ -88,9 +88,9 @@ func axObserverApplicationCallback(observer: AXObserver, element: AXUIElement, n
|
||||
case kAXWindowCreatedNotification:
|
||||
guard element.isActualWindow() else { return }
|
||||
// a window being un-minimized can trigger kAXWindowCreatedNotification
|
||||
guard Windows.listRecentlyUsedFirst.firstIndexThatMatches(element) == nil else { return }
|
||||
guard Windows.list.firstIndexThatMatches(element) == nil else { return }
|
||||
let window = Window(element, application)
|
||||
Windows.listRecentlyUsedFirst.insert(window, at: 0)
|
||||
Windows.list.insert(window, at: 0)
|
||||
Windows.moveFocusedWindowIndexAfterWindowCreatedInBackground()
|
||||
// TODO: find a better way to get thumbnail of the new window
|
||||
window.refreshThumbnail()
|
||||
@@ -98,8 +98,8 @@ func axObserverApplicationCallback(observer: AXObserver, element: AXUIElement, n
|
||||
case kAXFocusedWindowChangedNotification:
|
||||
guard !app.appIsBeingUsed,
|
||||
element.isActualWindow(),
|
||||
let existingIndex = Windows.listRecentlyUsedFirst.firstIndexThatMatches(element) else { return }
|
||||
Windows.listRecentlyUsedFirst.insert(Windows.listRecentlyUsedFirst.remove(at: existingIndex), at: 0)
|
||||
let existingIndex = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
Windows.list.insert(Windows.list.remove(at: existingIndex), at: 0)
|
||||
default: return
|
||||
}
|
||||
}
|
||||
@@ -2,52 +2,44 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Applications {
|
||||
static var map = [pid_t: Application]()
|
||||
static var list = [Application]()
|
||||
static var appsObserver = RunningApplicationsObserver()
|
||||
|
||||
static func addInitialRunningApplications() {
|
||||
addRunningApplications(NSWorkspace.shared.runningApplications)
|
||||
}
|
||||
|
||||
static func addRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||
for app in filterApplications(runningApps) {
|
||||
Applications.map[app.processIdentifier] = Application(app)
|
||||
}
|
||||
}
|
||||
|
||||
static func observeRunningApplications() {
|
||||
NSWorkspace.shared.addObserver(Applications.appsObserver, forKeyPath: "runningApplications", options: [.old, .new], context: nil)
|
||||
}
|
||||
|
||||
static func reviewRunningApplicationsWindows() {
|
||||
for app in map.values {
|
||||
for app in list {
|
||||
guard app.runningApplication.isFinishedLaunching else { continue }
|
||||
app.observeNewWindows()
|
||||
}
|
||||
}
|
||||
|
||||
static func removeApplications(_ runningApps: [NSRunningApplication]) {
|
||||
var someAppsAreAlreadyTerminated = false
|
||||
static func addRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||
for app in filterApplications(runningApps) {
|
||||
Applications.list.append(Application(app))
|
||||
}
|
||||
}
|
||||
|
||||
static func removeRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||
for runningApp in runningApps {
|
||||
guard runningApp.bundleIdentifier != nil else { someAppsAreAlreadyTerminated = true; continue }
|
||||
guard let app = Applications.map[runningApp.processIdentifier] else { continue }
|
||||
guard let app = Applications.list.first(where: { $0.runningApplication.isEqual(runningApp) }) else { continue }
|
||||
var windowsToKeep = [Window]()
|
||||
for window in Windows.listRecentlyUsedFirst {
|
||||
guard window.application.runningApplication.processIdentifier != runningApp.processIdentifier else { continue }
|
||||
for window in Windows.list {
|
||||
guard !window.application.runningApplication.isEqual(runningApp) else { continue }
|
||||
windowsToKeep.append(window)
|
||||
}
|
||||
Windows.listRecentlyUsedFirst = windowsToKeep
|
||||
Windows.list = windowsToKeep
|
||||
// some apps never finish launching; the observer leaks for them without this
|
||||
app.removeObserver()
|
||||
Applications.map.removeValue(forKey: runningApp.processIdentifier)
|
||||
Applications.list.removeAll(where: { $0.runningApplication.isEqual(runningApp) })
|
||||
}
|
||||
// sometimes removed `runningApps` are already terminated by the time they reach this method so we can't match their pid in `Applications.map` above
|
||||
// we need to remove them based on their lack of `bundleIdentifier`
|
||||
if someAppsAreAlreadyTerminated {
|
||||
Windows.listRecentlyUsedFirst.removeAll(where: { $0.application.runningApplication.bundleIdentifier == nil })
|
||||
Applications.map = Applications.map.filter { $0.value.runningApplication.bundleIdentifier != nil }
|
||||
}
|
||||
guard Windows.listRecentlyUsedFirst.count > 0 else { (App.shared as! App).hideUi(); return }
|
||||
guard Windows.list.count > 0 else { (App.shared as! App).hideUi(); return }
|
||||
// TODO: implement of more sophisticated way to decide which thumbnail gets focused on app quit
|
||||
Windows.focusedWindowIndex = 1
|
||||
(App.shared as! App).refreshOpenUi()
|
||||
@@ -71,7 +63,7 @@ class RunningApplicationsObserver: NSObject {
|
||||
case .removal:
|
||||
let apps = change![.oldKey] as! [NSRunningApplication]
|
||||
debugPrint("OS event: apps quit", apps.map { ($0.processIdentifier, $0.bundleIdentifier) })
|
||||
Applications.removeApplications(apps)
|
||||
Applications.removeRunningApplications(apps)
|
||||
default: return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,20 +104,20 @@ func axObserverWindowCallback(observer: AXObserver, element: AXUIElement, notifi
|
||||
debugPrint("OS event: " + type, element.title())
|
||||
switch type {
|
||||
case kAXUIElementDestroyedNotification:
|
||||
guard let existingIndex = Windows.listRecentlyUsedFirst.firstIndexThatMatches(element) else { return }
|
||||
Windows.listRecentlyUsedFirst.remove(at: existingIndex)
|
||||
guard Windows.listRecentlyUsedFirst.count > 0 else { app.hideUi(); return }
|
||||
guard let existingIndex = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
Windows.list.remove(at: existingIndex)
|
||||
guard Windows.list.count > 0 else { app.hideUi(); return }
|
||||
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(existingIndex)
|
||||
app.refreshOpenUi()
|
||||
case kAXWindowMiniaturizedNotification, kAXWindowDeminiaturizedNotification:
|
||||
guard let window = Windows.listRecentlyUsedFirst.firstWindowThatMatches(element) else { return }
|
||||
guard let window = Windows.list.firstWindowThatMatches(element) else { return }
|
||||
window.isMinimized = type == kAXWindowMiniaturizedNotification
|
||||
// TODO: find a better way to get thumbnail of the new window (when AltTab is triggered min/demin animation)
|
||||
window.refreshThumbnail()
|
||||
app.refreshOpenUi()
|
||||
case kAXTitleChangedNotification:
|
||||
guard element.isActualWindow(),
|
||||
let window = Windows.listRecentlyUsedFirst.firstWindowThatMatches(element),
|
||||
let window = Windows.list.firstWindowThatMatches(element),
|
||||
let newTitle = window.axUiElement.title(),
|
||||
newTitle != window.title else { return }
|
||||
window.title = newTitle
|
||||
|
||||
@@ -2,15 +2,16 @@ import Cocoa
|
||||
import Foundation
|
||||
|
||||
class Windows {
|
||||
static var listRecentlyUsedFirst = [Window]()
|
||||
// order in the array is important: most-recently-used elements are first
|
||||
static var list = [Window]()
|
||||
static var focusedWindowIndex = Array<Window>.Index(0)
|
||||
|
||||
static func focusedWindow() -> Window? {
|
||||
return listRecentlyUsedFirst.count > focusedWindowIndex ? listRecentlyUsedFirst[focusedWindowIndex] : nil
|
||||
return list.count > focusedWindowIndex ? list[focusedWindowIndex] : nil
|
||||
}
|
||||
|
||||
static func cycleFocusedWindowIndex(_ step: Array<Window>.Index) {
|
||||
focusedWindowIndex = focusedWindowIndex + step < 0 ? listRecentlyUsedFirst.count - 1 : (focusedWindowIndex + step) % listRecentlyUsedFirst.count
|
||||
focusedWindowIndex = focusedWindowIndex + step < 0 ? list.count - 1 : (focusedWindowIndex + step) % list.count
|
||||
}
|
||||
|
||||
static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ destroyedWindowIndex: Array<Window>.Index) {
|
||||
@@ -26,7 +27,7 @@ class Windows {
|
||||
|
||||
static func updateSpaces() {
|
||||
let spacesMap = Spaces.allIdsAndIndexes()
|
||||
for window in listRecentlyUsedFirst {
|
||||
for window in list {
|
||||
guard let spaceId = (CGSCopySpacesForWindows(cgsMainConnectionId, CGSSpaceMask.all.rawValue, [window.cgWindowId] as CFArray) as! [CGSSpaceID]).first else { continue }
|
||||
window.spaceId = spaceId
|
||||
window.spaceIndex = spacesMap.first { $0.0 == spaceId }!.1
|
||||
@@ -38,7 +39,7 @@ class Windows {
|
||||
for (index, cgWindowId) in Spaces.windowsInSpaces([Spaces.currentSpaceId]).enumerated() {
|
||||
windowLevelMap[cgWindowId] = index
|
||||
}
|
||||
var sortedTuples = Windows.listRecentlyUsedFirst.map { (windowLevelMap[$0.cgWindowId], $0) }
|
||||
var sortedTuples = Windows.list.map { (windowLevelMap[$0.cgWindowId], $0) }
|
||||
sortedTuples.sort(by: {
|
||||
if $0.0 == nil {
|
||||
return false
|
||||
@@ -48,25 +49,12 @@ class Windows {
|
||||
}
|
||||
return $0.0! < $1.0!
|
||||
})
|
||||
Windows.listRecentlyUsedFirst = sortedTuples.map { $0.1 }
|
||||
Windows.list = sortedTuples.map { $0.1 }
|
||||
}
|
||||
|
||||
static func refreshAllThumbnails() {
|
||||
for window in listRecentlyUsedFirst {
|
||||
for window in list {
|
||||
window.refreshThumbnail()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == Window {
|
||||
func firstIndexThatMatches(_ element: AXUIElement) -> Self.Index? {
|
||||
// `CFEqual` is safer than comparing `CGWindowID` because it will succeed even if the window is deallocated
|
||||
// by the OS, in which case the `CGWindowID` will be `-1`
|
||||
return firstIndex(where: { CFEqual($0.axUiElement, element) })
|
||||
}
|
||||
|
||||
func firstWindowThatMatches(_ element: AXUIElement) -> Window? {
|
||||
guard let index = firstIndexThatMatches(element) else { return nil }
|
||||
return self[index]
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class App: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
if isFirstSummon {
|
||||
debugPrint("showUiOrCycleSelection: isFirstSummon")
|
||||
isFirstSummon = false
|
||||
if Windows.listRecentlyUsedFirst.count == 0 || CGWindow.isMissionControlActive() {
|
||||
if Windows.list.count == 0 || CGWindow.isMissionControlActive() {
|
||||
appIsBeingUsed = false
|
||||
isFirstSummon = true
|
||||
return
|
||||
|
||||
@@ -62,18 +62,18 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return Windows.listRecentlyUsedFirst.count
|
||||
return Windows.list.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
||||
let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell
|
||||
item.updateWithNewContent(Windows.listRecentlyUsedFirst[indexPath.item], app!.focusSelectedWindow, app!.thumbnailsPanel!.highlightCell, currentScreen!)
|
||||
item.updateWithNewContent(Windows.list[indexPath.item], app!.focusSelectedWindow, app!.thumbnailsPanel!.highlightCell, currentScreen!)
|
||||
return item
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
|
||||
if indexPath.item < Windows.listRecentlyUsedFirst.count {
|
||||
let (width, height) = Cell.computeDownscaledSize(Windows.listRecentlyUsedFirst[indexPath.item].thumbnail, currentScreen!)
|
||||
if indexPath.item < Windows.list.count {
|
||||
let (width, height) = Cell.computeDownscaledSize(Windows.list[indexPath.item].thumbnail, currentScreen!)
|
||||
return NSSize(width: CGFloat(width) + Preferences.cellPadding * 2, height: CGFloat(height) + max(Preferences.fontHeight!, Preferences.iconSize!) + Preferences.interItemPadding + Preferences.cellPadding * 2)
|
||||
}
|
||||
return .zero
|
||||
|
||||
Reference in new issue
Block a user