18

How would I programmatically activate i.e move-to-front-and-focus a window on macOS (not belonging to my app) given its Window ID. My app would run with user granted Accessibility permissions etc.

Surprisingly, none of the functions described on the Quartz Window Services page seem to do that.

Am using Swift currently, but am open to using Objective-C, AppleScript or whatever.

EDIT:

I don't want to bring to front all windows of the parent app - only the specific that matches the window ID.

Edit:

I know that the NSWindow type is only meant to refer to windows of the current process, but is there no class that represents windows owned by external apps? Like we have NSRunningApplication to refer to any running app including external ones, I was expecting an API to deal all open windows (assuming the right permissions). Is there some class like NSOpenWindow or CGWindow buried somewhere?

Himanshu P
  • 9,586
  • 6
  • 37
  • 46
  • Please be more specific, what is `Window ID` and how did you get it? – Hexfire Nov 07 '17 at 12:26
  • 1
    https://developer.apple.com/documentation/coregraphics/kcgwindownumber – Himanshu P Nov 07 '17 at 15:06
  • The window ID is one of the required values present in the window information returned by `CGWindowListCopyWindowInfo()` – Himanshu P Nov 07 '17 at 15:13
  • Is the parent app scriptable? Is the Accessibility API an option? – Willeke Nov 08 '17 at 00:37
  • Yes, my app would run with user granted accessibility permissions if required. The windows I would want to activate could belong to any app. – Himanshu P Nov 09 '17 at 08:14
  • @HimanshuP Have you seen this? https://stackoverflow.com/questions/1730859/controlling-osx-windows – hidefromkgb Nov 09 '17 at 11:22
  • Yes I have, it doesn't have any reference to Window IDs as far as I could tell. – Himanshu P Nov 09 '17 at 16:11
  • Do you need `CGWindowListCopyWindowInfo()` or can you get the windows using the Accessibility API or AppleScript? – Willeke Nov 10 '17 at 05:22
  • For some reason using the Accessibility API's AXUIElementCopyAttributeValue() has been returning AXError.cannotComplete (using Swift). I found a couple of posts about this issue on StackOverflow with no answers. – Himanshu P Nov 10 '17 at 09:34
  • It works if the app isn't sandboxed. [How to use Accessibility with sandboxed app?](https://stackoverflow.com/questions/32116095/how-to-use-accessibility-with-sandboxed-app) – Willeke Nov 11 '17 at 02:32
  • 1
    Did you manage to do this? I'm trying to achieve the same. – Marie Dm Jun 14 '18 at 14:31

2 Answers2

6

I didn't find a way to switch to a specific window yet, but you can switch to the app that contains a specific window using this function:

func switchToApp(withWindow windowNumber: Int32) {
    let options = CGWindowListOption(arrayLiteral: CGWindowListOption.excludeDesktopElements, CGWindowListOption.optionOnScreenOnly)
    let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
    guard let infoList = windowListInfo as NSArray? as? [[String: AnyObject]] else { return }
    if let window = infoList.first(where: { ($0["kCGWindowNumber"] as? Int32) == windowNumber}), let pid = window["kCGWindowOwnerPID"] as? Int32 {
        let app = NSRunningApplication(processIdentifier: pid)
        app?.activate(options: .activateIgnoringOtherApps)
    }
}

It is probably usefull to switch by name as well:

func switchToApp(named windowOwnerName: String) {
    let options = CGWindowListOption(arrayLiteral: CGWindowListOption.excludeDesktopElements, CGWindowListOption.optionOnScreenOnly)
    let windowListInfo = CGWindowListCopyWindowInfo(options, CGWindowID(0))
    guard let infoList = windowListInfo as NSArray? as? [[String: AnyObject]] else { return }

    if let window = infoList.first(where: { ($0["kCGWindowOwnerName"] as? String) == windowOwnerName}), let pid = window["kCGWindowOwnerPID"] as? Int32 {
        let app = NSRunningApplication(processIdentifier: pid)
        app?.activate(options: .activateIgnoringOtherApps)
    }
}

Example: switchToApp(named: "OpenOffice")

On my mac OpenOffice was started with a window with kCGWindowNumber = 599, so this has the same effect: switchToApp(withWindow: 599)

As far as I found out so far, your options seem to be to show the currently active window of the app, or to show all windows (using .activateAllWindows as activation option)

Daniel
  • 20,420
  • 10
  • 92
  • 149
  • 2
    Is there a way to activate a specific window of the app? Example: Firefox is active and has 5 windows. I want to activate one in particular, thanks to its `kCGWindowNumber`. – Marie Dm Jun 10 '18 at 12:15
4

For anyone looking for an Objective C solution:

#import <Cocoa/Cocoa.h>
#import <libproc.h>
#import <string.h>
#import <stdlib.h>
#import <stdio.h>

bool activate_window_of_id(unsigned long wid) {
  bool success = false;
  const CGWindowLevel kScreensaverWindowLevel = CGWindowLevelForKey(kCGScreenSaverWindowLevelKey);
  CFArrayRef windowArray = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID);
  CFIndex windowCount = 0;
  if ((windowCount = CFArrayGetCount(windowArray))) {
    for (CFIndex i = 0; i < windowCount; i++) {
      NSDictionary *windowInfoDictionary = (__bridge NSDictionary *)((CFDictionaryRef)CFArrayGetValueAtIndex(windowArray, i));
      NSNumber *ownerPID = (NSNumber *)(windowInfoDictionary[(id)kCGWindowOwnerPID]);
      NSNumber *level = (NSNumber *)(windowInfoDictionary[(id)kCGWindowLayer]);
      if (level.integerValue < kScreensaverWindowLevel) {
        NSNumber *windowID = windowInfoDictionary[(id)kCGWindowNumber];
        if (wid == windowID.integerValue) {
          CFIndex appCount = [[[NSWorkspace sharedWorkspace] runningApplications] count];
          for (CFIndex j = 0; j < appCount; j++) {
            if (ownerPID.integerValue == [[[[NSWorkspace sharedWorkspace] runningApplications] objectAtIndex:j] processIdentifier]) {
              NSRunningApplication *appWithPID = [[[NSWorkspace sharedWorkspace] runningApplications] objectAtIndex:j];
              [appWithPID activateWithOptions:NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps];
              char buf[PROC_PIDPATHINFO_MAXSIZE];
              proc_pidpath(ownerPID.integerValue, buf, sizeof(buf));
              NSString *buffer = [NSString stringWithUTF8String:buf];
              unsigned long location = [buffer rangeOfString:@".app/Contents/MacOS/" options:NSBackwardsSearch].location;
              NSString *path = (location != NSNotFound) ? [buffer substringWithRange:NSMakeRange(0, location)] : buffer;
              NSString *app = [@" of application \\\"" stringByAppendingString:[path lastPathComponent]];
              NSString *index = [@"set index of window id " stringByAppendingString:[windowID stringValue]];
              NSString *execScript = [[index stringByAppendingString:app] stringByAppendingString:@"\\\" to 1"];
              char *pointer = NULL;
              size_t buffer_size = 0;
              NSMutableArray *array = [[NSMutableArray alloc] init];
              FILE *file = popen([[[@"osascript -e \"" stringByAppendingString:execScript] stringByAppendingString:@"\" 2>&1"] UTF8String], "r");
              while (getline(&pointer, &buffer_size, file) != -1)
                [array addObject:[NSString stringWithUTF8String:pointer]];
              char *error = (char *)[[array componentsJoinedByString:@""] UTF8String];
              if (strlen(error) > 0 && error[strlen(error) - 1] == '\n')
                error[strlen(error) - 1] = '\0';
              if ([[NSString stringWithUTF8String:error] isEqualToString:@""])
                success = true;
              [array release];
              free(pointer);
              pclose(file);
              break;
            }
          }
        }
      }
    }
  }
  CFRelease(windowArray);
  return success;
}

Note, unlike Daniel's answer, this will not just bring the specified application's windows to the front, it will also make sure the specific window whose id matches the one specified will be the topmost out of that app's collection of windows. It will return true on success, and false on failure. I noticed it brings to front for some apps but not for others. I'm not sure why. The code it is based on does not work as advertised for its original purpose. Although, it did help me a lot to get working all the stuff I needed to answer this question. The code my answer is based on can be found here. Ignore the original usage.

  • This seems to works, but it will prompt `MyApp wants to access control AppName. Allowing control will provide access to documents and data in AppName, and to perform action within that app` – Tj3n Sep 22 '21 at 07:42
  • 1
    Yeah, I'm not sure if there is any way around that. Apple pretty much restricts all their third party developers from doing all kinds of important IPC that involves gui manipulation, and it's annoying because they do it to gain trust from their users to make it look like they care at all about the user and their privacy and personal protection, when they violate your personal space and know everything you do on your mac. It's only ok to be "invasive" when you are apple apparently. –  May 04 '22 at 14:15
  • For those who may not want to run osascript can look into NSAppleScript and have it run the script directly from your app as a singular app/process. Although running via osascript is probably safer as it won't crash or have undefined behavior whenever applescript has internal changes done to it after an OS update. –  May 04 '22 at 14:19