Skip to content

Commit

Permalink
fix: quitting apps was not properly removing apps from the list
Browse files Browse the repository at this point in the history
Instead of using the PSN, we now use .equals() to compare 2 NSRunningApplication as recommended by Apple docs
  • Loading branch information
louis.pontoise authored and lwouis committed Mar 10, 2020
1 parent bfc2700 commit 10b2c71
Show file tree
Hide file tree
Showing 7 changed files with 56 additions and 63 deletions.
13 changes: 13 additions & 0 deletions alt-tab-macos/api-wrappers/HelperExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
}
18 changes: 9 additions & 9 deletions alt-tab-macos/logic/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]) {
Expand All @@ -77,29 +77,29 @@ 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
}
app.refreshOpenUi()
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()
app.refreshOpenUi()
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
}
}
40 changes: 16 additions & 24 deletions alt-tab-macos/logic/Applications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
// 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 }
Applications.list.removeAll(where: { $0.runningApplication.isEqual(runningApp) })
}
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()
Expand All @@ -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
}
}
Expand Down
10 changes: 5 additions & 5 deletions alt-tab-macos/logic/Window.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 8 additions & 20 deletions alt-tab-macos/logic/Windows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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]
}
}
2 changes: 1 addition & 1 deletion alt-tab-macos/ui/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions alt-tab-macos/ui/ThumbnailsPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 10b2c71

Please sign in to comment.