kCGWindowListOptionOnScreenOnly wrong window order

I created an AX observer for kAXMainWindowChangedNotification. In the callback function I call CGWindowListCopyWindowInfo with kCGWindowListOptionOnScreenOnly option to get the id of the current main window (Windows are filtered by PID because I am only interested in the frontmost app). What I experienced is that the received window order reflects the previous state (before the window switch). If I delay the call of CGWindowListCopyWindowInfo with a few milliseconds then the order is perfect but it does not seem to be a stable solution. Is there any other way to wait until every API is notified about the changes then call CGWindowListCopyWindowInfo to get the most recent window information?

Subscription:

  AXUIElementRef appElem = AXUIElementCreateApplication(processId.intValue);

CFArrayRef windows;
  AXError copyResult = AXUIElementCopyAttributeValues(appElem, kAXWindowsAttribute, 0, 1, &windows);

AXError createResult = AXObserverCreate(processId.intValue, windowSwitchedCallback, &observer);
   
if (copyResult != createResult != kAXErrorSuccess) {
   return;
}

  AXObserverAddNotification(observer, appElem, kAXMainWindowChangedNotification, (__bridge void *)(self));
  CFRunLoopAddSource([[NSRunLoop currentRunLoop] getCFRunLoop], AXObserverGetRunLoopSource(observer), kCFRunLoopDefaultMode);

Callback:

void windowSwitchedCallback(AXObserverRef observer, AXUIElementRef element, CFStringRef notificationName, void *refCon) {
if (CFStringCompare(notificationName, kAXMainWindowChangedNotification, 0) == kCFCompareEqualTo) {
    NSTimeInterval delayInMSec = 10;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInMSec * NSEC_PER_MSEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
      NSDictionary *activeWindow = [(__bridge ActiveWindowObserver*)(refCon) getActiveWindow];
      NSLog(@"%@ windowChanged",activeWindow);
    });
  }
}

ActiveWindowObserver:

- (NSDictionary*) getActiveWindow
{
  CFArrayRef windowsRef = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID);
  NSArray *windowsArray = (NSArray *)CFBridgingRelease(windowsRef);
  NSPredicate *pIdPredicate = [NSPredicate predicateWithFormat:@"kCGWindowOwnerPID == %@ && kCGWindowLayer == 0", processId];
  NSArray *filteredWindows = [windowsArray filteredArrayUsingPredicate:pIdPredicate];
  id activeWindow = filteredWindows.count > 0 ? filteredWindows.firstObject : nil;
  return activeWindow;
}

Replies

I'm glad to hear that adding a delay is helping you work around this, but I agree that this isn't a fantastic solution. Our team wants to help with this, could you please take a few minutes to file a bug using Feedback Assistant, with as much code examples (like the awesome ones you provided here) and a sample project if you're comfortable. This will make sure we get this request into our system.

Here's the link if you need it, https://developer.apple.com/bug-reporting/

Add a Comment