11import AppKit
2+ import AsyncExtensions
23import AXExtension
34import AXNotificationStream
45import 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