-
-
Notifications
You must be signed in to change notification settings - Fork 355
/
Copy pathAccessibilityEvents.swift
201 lines (190 loc) · 9.45 KB
/
AccessibilityEvents.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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import Cocoa
import ApplicationServices.HIServices.AXUIElement
import ApplicationServices.HIServices.AXNotificationConstants
let axObserverCallback: AXObserverCallback = { _, element, notificationName, _ in
let type = notificationName as String
Logger.debug(type)
AXUIElement.retryAxCallUntilTimeout { try handleEvent(type, element) }
}
fileprivate func handleEvent(_ type: String, _ element: AXUIElement) throws {
// events are handled concurrently, thus we check that the app is still running
if let pid = try element.pid(),
try pid != ProcessInfo.processInfo.processIdentifier || (element.subrole() != kAXUnknownSubrole) {
Logger.info(type, pid, try element.title() ?? "nil")
switch type {
case kAXApplicationActivatedNotification: try applicationActivated(element, pid)
case kAXApplicationHiddenNotification,
kAXApplicationShownNotification: try applicationHiddenOrShown(pid, type)
case kAXWindowCreatedNotification: try windowCreated(element, pid)
case kAXMainWindowChangedNotification,
kAXFocusedWindowChangedNotification: try focusedWindowChanged(element, pid)
case kAXUIElementDestroyedNotification: try windowDestroyed(element, pid)
case kAXWindowMiniaturizedNotification,
kAXWindowDeminiaturizedNotification: try windowMiniaturizedOrDeminiaturized(element, type)
case kAXTitleChangedNotification: try windowTitleChanged(element)
case kAXWindowResizedNotification,
kAXWindowMovedNotification: try windowResizedOrMoved(element)
default: return
}
}
}
fileprivate func applicationActivated(_ element: AXUIElement, _ pid: pid_t) throws {
let appFocusedWindow = try element.focusedWindow()
let wid = try appFocusedWindow?.cgWindowId()
DispatchQueue.main.async {
if let app = Applications.find(pid) {
if app.hasBeenActiveOnce != true {
app.hasBeenActiveOnce = true
}
let window = (appFocusedWindow != nil && wid != nil) ? Windows.updateLastFocus(appFocusedWindow!, wid!)?.first : nil
app.focusedWindow = window
App.app.checkIfShortcutsShouldBeDisabled(window, app.runningApplication)
App.app.refreshOpenUi(window != nil ? [window!] : [], .refreshUiAfterExternalEvent)
}
}
}
fileprivate func applicationHiddenOrShown(_ pid: pid_t, _ type: String) throws {
DispatchQueue.main.async {
if let app = Applications.find(pid) {
app.isHidden = type == kAXApplicationHiddenNotification
let windows = Windows.list.filter {
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
return $0.application.pid == pid
}
// if we process the "shown" event too fast, the window won't be listed by CGSCopyWindowsWithOptionsAndTags
// it will thus be detected as isTabbed. We add a delay to work around this scenario
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
}
}
}
}
fileprivate func windowCreated(_ element: AXUIElement, _ pid: pid_t) throws {
if let wid = try element.cgWindowId(),
let (title, role, subrole, isMinimized, isFullscreen) = try element.windowAttributes() {
let position = try element.position()
let size = try element.size()
let level = wid.level()
DispatchQueue.main.async {
if let app = Applications.find(pid), NSRunningApplication(processIdentifier: pid) != nil {
if (!Windows.list.contains { $0.isEqualRobust(element, wid) }) &&
AXUIElement.isActualWindow(app, wid, level, title, subrole, role, size) {
let window = Window(element, app, wid, title, isFullscreen, isMinimized, position, size)
Windows.appendAndUpdateFocus(window)
Windows.cycleFocusedWindowIndex(1)
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
}
}
fileprivate func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) throws {
if let wid = try element.cgWindowId(),
let runningApp = NSRunningApplication(processIdentifier: pid) {
// photoshop will focus a window *after* you focus another app
// we check that a focused window happens within an active app
if runningApp.isActive {
DispatchQueue.main.async {
guard let app = Applications.find(pid) else { return }
// if the window is shown by alt-tab, we mark it as focused for this app
// this avoids issues with dialogs, quicklook, etc (see scenarios from #1044 and #2003)
if let w = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
app.focusedWindow = w
}
if let windows = Windows.updateLastFocus(element, wid) {
App.app.refreshOpenUi(windows, .refreshUiAfterExternalEvent)
} else {
AXUIElement.retryAxCallUntilTimeout {
if let (title, role, subrole, isMinimized, isFullscreen) = try element.windowAttributes() {
let position = try element.position()
let size = try element.size()
let level = wid.level()
DispatchQueue.main.async {
if (!Windows.list.contains { $0.isEqualRobust(element, wid) }),
AXUIElement.isActualWindow(app, wid, level, title, subrole, role, size) {
let window = Window(element, app, wid, title, isFullscreen, isMinimized, position, size)
Windows.appendAndUpdateFocus(window)
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
}
}
}
}
} else {
DispatchQueue.main.async {
Applications.find(pid)?.focusedWindow = nil
}
}
}
fileprivate func windowDestroyed(_ element: AXUIElement, _ pid: pid_t) throws {
let wid = try element.cgWindowId()
DispatchQueue.main.async {
if let index = (Windows.list.firstIndex { $0.isEqualRobust(element, wid) }) {
let window = Windows.list[index]
Windows.removeAndUpdateFocus(window)
if window.application.addWindowlessWindowIfNeeded() != nil {
Applications.find(pid)?.focusedWindow = nil
}
if Windows.list.count > 0 {
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(index)
App.app.refreshOpenUi([], .refreshUiAfterExternalEvent)
} else {
App.app.hideUi()
}
}
}
}
fileprivate func windowMiniaturizedOrDeminiaturized(_ element: AXUIElement, _ type: String) throws {
if let wid = try element.cgWindowId() {
DispatchQueue.main.async {
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }) {
window.isMinimized = type == kAXWindowMiniaturizedNotification
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
}
fileprivate func windowTitleChanged(_ element: AXUIElement) throws {
if let wid = try element.cgWindowId() {
AXUIElement.retryAxCallUntilTimeoutDebounced(.windowTitleChanged, wid) {
if let (title, _, _, isMinimized, isFullscreen) = try element.windowAttributes() {
DispatchQueue.main.async {
if let window = (Windows.list.first { $0.isEqualRobust(element, wid) }), title != window.title {
window.title = window.bestEffortTitle(title)
window.isMinimized = isMinimized
window.isFullscreen = isFullscreen
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
}
}
}
fileprivate func windowResizedOrMoved(_ element: AXUIElement) throws {
if let wid = try element.cgWindowId() {
AXUIElement.retryAxCallUntilTimeoutDebounced(.windowResizedOrMoved, wid) {
try updateWindowSizeAndPositionAndFullscreen(element, wid, nil)
}
}
}
func updateWindowSizeAndPositionAndFullscreen(_ element: AXUIElement, _ wid: CGWindowID, _ window: Window?) throws {
if let (title, _, _, isMinimized, isFullscreen) = try element.windowAttributes() {
let size = try element.size()
let position = try element.position()
DispatchQueue.main.async {
if let window = (window != nil ? window : (Windows.list.first { $0.isEqualRobust(element, wid) })) {
window.title = title
window.size = size
window.position = position
window.isMinimized = isMinimized
if window.isFullscreen != isFullscreen {
window.isFullscreen = isFullscreen
App.app.checkIfShortcutsShouldBeDisabled(window, nil)
}
App.app.refreshOpenUi([window], .refreshUiAfterExternalEvent)
}
}
}
}