-
-
Notifications
You must be signed in to change notification settings - Fork 348
/
Copy pathApplication.swift
165 lines (155 loc) · 7.79 KB
/
Application.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import Cocoa
import ApplicationServices.HIServices.AXNotificationConstants
class Application: NSObject {
// kvObservers should be listed first, so it gets deinit'ed first; otherwise it can crash
var kvObservers: [NSKeyValueObservation]?
var runningApplication: NSRunningApplication
var axUiElement: AXUIElement?
var axObserver: AXObserver?
var isReallyFinishedLaunching = false
var isHidden: Bool!
var icon: NSImage?
var dockLabel: String?
var pid: pid_t { runningApplication.processIdentifier }
var wasLaunchedBeforeAltTab = false
static func notifications(_ app: NSRunningApplication) -> [String] {
let n = [
kAXApplicationActivatedNotification,
kAXMainWindowChangedNotification,
kAXFocusedWindowChangedNotification,
kAXWindowCreatedNotification,
kAXApplicationHiddenNotification,
kAXApplicationShownNotification,
kAXFocusedUIElementChangedNotification,
]
// workaround: some apps exhibit bugs when we subscribe to its kAXFocusedUIElementChangedNotification
// we don't know what's happening; we avoid this subscription to make these app usable
if app.bundleIdentifier == "edu.stanford.protege" || app.bundleIdentifier?.range(of: "^com\\.jetbrains\\..+?EAP$", options: .regularExpression) != nil {
return n.filter { $0 != kAXFocusedUIElementChangedNotification }
}
return n
}
init(_ runningApplication: NSRunningApplication) {
self.runningApplication = runningApplication
super.init()
isHidden = runningApplication.isHidden
icon = runningApplication.icon
addAndObserveWindows()
kvObservers = [
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in
guard let self = self else { return }
self.addAndObserveWindows()
},
runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in
guard let self = self else { return }
if self.runningApplication.activationPolicy != .regular {
self.removeWindowslessAppWindow()
}
self.addAndObserveWindows()
},
]
}
deinit {
debugPrint("Deinit app", runningApplication.bundleIdentifier ?? runningApplication.bundleURL ?? "nil")
}
func removeWindowslessAppWindow() {
if let windowlessAppWindow = (Windows.list.firstIndex { $0.isWindowlessApp == true && $0.application.pid == pid }) {
Windows.list.remove(at: windowlessAppWindow)
App.app.refreshOpenUi()
}
}
func addAndObserveWindows() {
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited && axUiElement == nil {
axUiElement = AXUIElementCreateApplication(pid)
AXObserverCreate(pid, axObserverCallback, &axObserver)
debugPrint("Adding app", pid, runningApplication.bundleIdentifier ?? "nil")
observeEvents()
}
}
func observeNewWindows(_ group: DispatchGroup? = nil) {
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
retryAxCallUntilTimeout(group, 5) { [weak self] in
guard let self = self else { return }
if let axWindows_ = try self.axUiElement!.windows(), axWindows_.count > 0 {
// bug in macOS: sometimes the OS returns multiple duplicate windows (e.g. Mail.app starting at login)
let axWindows = try Array(Set(axWindows_)).compactMap {
if let wid = try $0.cgWindowId() {
let title = try $0.title()
let subrole = try $0.subrole()
let role = try $0.role()
let size = try $0.size()
let isOnNormalLevel = $0.isOnNormalLevel(wid)
if $0.isActualWindow(self.runningApplication, wid, isOnNormalLevel, title, subrole, role, size) {
return ($0, wid, title, try $0.isFullscreen(), try $0.isMinimized(), try $0.position())
}
}
return nil
} as [(AXUIElement, CGWindowID, String?, Bool, Bool, CGPoint?)]
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
var windows = self.addWindows(axWindows)
if let window = self.addWindowslessAppsIfNeeded() {
windows.append(contentsOf: window)
}
App.app.refreshOpenUi(windows)
}
} else {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
let window = self.addWindowslessAppsIfNeeded()
App.app.refreshOpenUi(window)
}
// workaround: opening an app while the active app is fullscreen; we wait out the space transition animation
if group == nil && !self.wasLaunchedBeforeAltTab && CGSSpaceGetType(cgsMainConnectionId, Spaces.currentSpaceId) == .fullscreen {
throw AxError.runtimeError
}
}
}
}
}
private func addWindows(_ axWindows: [(AXUIElement, CGWindowID, String?, Bool, Bool, CGPoint?)]) -> [Window] {
let windows: [Window] = axWindows.compactMap { (axUiElement, wid, axTitle, isFullscreen, isMinimized, position) in
if (Windows.list.firstIndex { $0.isEqualRobust(axUiElement, wid) }) == nil {
let window = Window(axUiElement, self, wid, axTitle, isFullscreen, isMinimized, position)
Windows.appendAndUpdateFocus(window)
return window
}
return nil
}
if App.app.appIsBeingUsed {
Windows.cycleFocusedWindowIndex(windows.count)
}
return windows
}
func addWindowslessAppsIfNeeded() -> [Window]? {
if !Preferences.hideWindowlessApps &&
runningApplication.activationPolicy == .regular &&
!runningApplication.isTerminated &&
(Windows.list.firstIndex { $0.application.pid == pid }) == nil {
let window = Window(self)
Windows.appendAndUpdateFocus(window)
return [window]
}
return nil
}
private func observeEvents() {
guard let axObserver = axObserver else { return }
for notification in Application.notifications(runningApplication) {
retryAxCallUntilTimeout { [weak self] in
guard let self = self else { return }
try self.axUiElement!.subscribeToNotification(axObserver, notification, {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// some apps have `isFinishedLaunching == true` but are actually not finished, and will return .cannotComplete
// we consider them ready when the first subscription succeeds, and list their windows again at that point
if !self.isReallyFinishedLaunching {
self.isReallyFinishedLaunching = true
self.observeNewWindows()
}
}
}, self.runningApplication)
}
}
CFRunLoopAddSource(BackgroundWork.accessibilityEventsThread.runLoop, AXObserverGetRunLoopSource(axObserver), .defaultMode)
}
}