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:
louis.pontoise authored and lwouis committed 2020-03-10 14:00:37 +09:00
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]
}
}
+9 -9
View File
@@ -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
}
}
+16 -24
View File
@@ -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
}
}
+5 -5
View File
@@ -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
+8 -20
View File
@@ -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]
}
}
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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