Skip to content

Commit 930780f

Browse files
committed
Handle AXNotification on app in XcodeAppInstanceInspector
1 parent 53d49ef commit 930780f

File tree

1 file changed

+125
-61
lines changed

1 file changed

+125
-61
lines changed

Tool/Sources/XcodeInspector/Apps/XcodeAppInstanceInspector.swift

Lines changed: 125 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AppKit
2+
import AsyncExtensions
23
import AXExtension
34
import AXNotificationStream
45
import Combine
@@ -15,6 +16,63 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
1516
return workspaces.mapValues(\.info)
1617
}
1718

19+
public struct AXNotification {
20+
public var kind: AXNotificationKind
21+
public var element: AXUIElement
22+
}
23+
24+
public enum AXNotificationKind {
25+
case applicationActivated
26+
case applicationDeactivated
27+
case moved
28+
case resized
29+
case mainWindowChanged
30+
case focusedWindowChanged
31+
case focusedUIElementChanged
32+
case windowMoved
33+
case windowResized
34+
case windowMiniaturized
35+
case windowDeminiaturized
36+
case created
37+
case uiElementDestroyed
38+
case xcodeCompletionPanelChanged
39+
40+
public init?(rawValue: String) {
41+
switch rawValue {
42+
case kAXApplicationActivatedNotification:
43+
self = .applicationActivated
44+
case kAXApplicationDeactivatedNotification:
45+
self = .applicationDeactivated
46+
case kAXMovedNotification:
47+
self = .moved
48+
case kAXResizedNotification:
49+
self = .resized
50+
case kAXMainWindowChangedNotification:
51+
self = .mainWindowChanged
52+
case kAXFocusedWindowChangedNotification:
53+
self = .focusedWindowChanged
54+
case kAXFocusedUIElementChangedNotification:
55+
self = .focusedUIElementChanged
56+
case kAXWindowMovedNotification:
57+
self = .windowMoved
58+
case kAXWindowResizedNotification:
59+
self = .windowResized
60+
case kAXWindowMiniaturizedNotification:
61+
self = .windowMiniaturized
62+
case kAXWindowDeminiaturizedNotification:
63+
self = .windowDeminiaturized
64+
case kAXCreatedNotification:
65+
self = .created
66+
case kAXUIElementDestroyedNotification:
67+
self = .uiElementDestroyed
68+
default:
69+
return nil
70+
}
71+
}
72+
}
73+
74+
public let axNotifications = AsyncPassthroughSubject<AXNotification>()
75+
1876
@Published public private(set) var completionPanel: AXUIElement?
1977

2078
public var realtimeDocumentURL: URL? {
@@ -66,6 +124,7 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
66124
private var focusedWindowObservations = Set<AnyCancellable>()
67125

68126
deinit {
127+
axNotifications.send(.finished)
69128
for task in longRunningTasks { task.cancel() }
70129
}
71130

@@ -75,9 +134,9 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
75134
Task { @MainActor in
76135
observeFocusedWindow()
77136
observeAXNotifications()
78-
137+
79138
try await Task.sleep(nanoseconds: 3_000_000_000)
80-
// Sometimes the focused window may not be ready on app launch.
139+
// Sometimes the focused window may not be rea?dy on app launch.
81140
if !(focusedWindow is WorkspaceXcodeWindowInspector) {
82141
observeFocusedWindow()
83142
}
@@ -90,7 +149,8 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
90149
if window.identifier == "Xcode.WorkspaceWindow" {
91150
let window = WorkspaceXcodeWindowInspector(
92151
app: runningApplication,
93-
uiElement: window
152+
uiElement: window,
153+
axNotifications: axNotifications
94154
)
95155
focusedWindow = window
96156

@@ -145,81 +205,85 @@ public final class XcodeAppInstanceInspector: AppInstanceInspector {
145205
longRunningTasks.forEach { $0.cancel() }
146206
longRunningTasks = []
147207

148-
let windowChangeNotification = AXNotificationStream(
208+
let axNotificationStream = AXNotificationStream(
149209
app: runningApplication,
150-
notificationNames: kAXFocusedWindowChangedNotification
210+
notificationNames:
211+
kAXApplicationActivatedNotification,
212+
kAXApplicationDeactivatedNotification,
213+
kAXMovedNotification,
214+
kAXResizedNotification,
215+
kAXMainWindowChangedNotification,
216+
kAXFocusedWindowChangedNotification,
217+
kAXFocusedUIElementChangedNotification,
218+
kAXWindowMovedNotification,
219+
kAXWindowResizedNotification,
220+
kAXWindowMiniaturizedNotification,
221+
kAXWindowDeminiaturizedNotification,
222+
kAXCreatedNotification,
223+
kAXUIElementDestroyedNotification
151224
)
152225

153-
let focusedWindowChanged = Task { @MainActor [weak self] in
154-
for await _ in windowChangeNotification {
226+
let observeAXNotificationTask = Task { @MainActor [weak self] in
227+
var updateWorkspaceInfoTask: Task<Void, Error>?
228+
229+
for await notification in axNotificationStream {
155230
guard let self else { return }
156231
try Task.checkCancellation()
157-
observeFocusedWindow()
158-
}
159-
}
160-
161-
longRunningTasks.insert(focusedWindowChanged)
162-
163-
updateWorkspaceInfo()
164-
165-
let elementChangeNotification = AXNotificationStream(
166-
app: runningApplication,
167-
notificationNames: kAXFocusedUIElementChangedNotification,
168-
kAXApplicationDeactivatedNotification
169-
)
170232

171-
let updateTabsTask = Task { @MainActor [weak self] in
172-
if #available(macOS 13.0, *) {
173-
for await _ in elementChangeNotification.debounce(for: .seconds(2)) {
174-
guard let self else { return }
175-
try Task.checkCancellation()
176-
updateWorkspaceInfo()
177-
}
178-
} else {
179-
for await _ in elementChangeNotification {
180-
guard let self else { return }
181-
try Task.checkCancellation()
182-
updateWorkspaceInfo()
233+
guard let event = AXNotificationKind(rawValue: notification.name) else {
234+
continue
183235
}
184-
}
185-
}
186236

187-
longRunningTasks.insert(updateTabsTask)
237+
self.axNotifications.send(.init(kind: event, element: notification.element))
188238

189-
let completionPanelNotification = AXNotificationStream(
190-
app: runningApplication,
191-
notificationNames: kAXCreatedNotification, kAXUIElementDestroyedNotification
192-
)
193-
194-
let completionPanelTask = Task { @MainActor [weak self] in
195-
for await event in completionPanelNotification {
196-
guard let self else { return }
239+
if event == .focusedWindowChanged {
240+
observeFocusedWindow()
241+
}
197242

198-
// We can only observe the creation and closing of the parent
199-
// of the completion panel.
200-
let isCompletionPanel = {
201-
event.element.identifier == "_XC_COMPLETION_TABLE_"
202-
|| event.element.firstChild { element in
203-
element.identifier == "_XC_COMPLETION_TABLE_"
204-
} != nil
243+
if event == .focusedUIElementChanged || event == .applicationDeactivated {
244+
updateWorkspaceInfoTask?.cancel()
245+
updateWorkspaceInfoTask = Task { [weak self] in
246+
guard let self else { return }
247+
try await Task.sleep(nanoseconds: 2_000_000_000)
248+
try Task.checkCancellation()
249+
self.updateWorkspaceInfo()
250+
}
205251
}
206-
switch event.name {
207-
case kAXCreatedNotification:
208-
if isCompletionPanel() {
209-
completionPanel = event.element
252+
253+
if event == .created || event == .uiElementDestroyed {
254+
let isCompletionPanel = {
255+
notification.element.identifier == "_XC_COMPLETION_TABLE_"
256+
|| notification.element.firstChild { element in
257+
element.identifier == "_XC_COMPLETION_TABLE_"
258+
} != nil
210259
}
211-
case kAXUIElementDestroyedNotification:
212-
if isCompletionPanel() {
213-
completionPanel = nil
260+
261+
switch event {
262+
case .created:
263+
if isCompletionPanel() {
264+
completionPanel = notification.element
265+
self.axNotifications.send(.init(
266+
kind: .xcodeCompletionPanelChanged,
267+
element: notification.element
268+
))
269+
}
270+
case .uiElementDestroyed:
271+
if isCompletionPanel() {
272+
completionPanel = nil
273+
self.axNotifications.send(.init(
274+
kind: .xcodeCompletionPanelChanged,
275+
element: notification.element
276+
))
277+
}
278+
default: continue
214279
}
215-
default: break
216280
}
217-
218-
try Task.checkCancellation()
219281
}
220282
}
221283

222-
longRunningTasks.insert(completionPanelTask)
284+
longRunningTasks.insert(observeAXNotificationTask)
285+
286+
updateWorkspaceInfo()
223287
}
224288
}
225289

0 commit comments

Comments
 (0)