Set non-owned window always on top - Like the app "Afloat"
Asked Answered
E

1

14

I have set up a global hotkey with RegisterEventHotkey. When the user presses it, it gets the currently focused window with CGWindowListCopyWindowInfo, and then I need to set it always on top.

If the current window is in my process (from which I am executing the code) I can simply convert the windowNumber from CGWindowListCopyWindowInfo to a NSWindow and do setLevel:

nswin = [NSApp windowWithWindowNumber:windowNumber]
[nswin setLevel: Int(CGWindowLevelForKey(kCGFloatingWindowLevelKey))]

My Problem I am not able to do this if the currently focused window is not in my process. Can you please show me how?

Stuff I tried:

My use: I'm trying to replicate the functionality my open source, free, browser addon - https://addons.mozilla.org/en-US/firefox/addon/topick/ - so my calling process if Firefox. It works on Windows and Linux right now, and just need to figure out how to do it in mac for non-Firefox windows.

Escalade answered 18/3, 2016 at 2:40 Comment(22)
Why don't you get NSWindow from CGWindowNumber?Fullbodied
Thanks @ElTomato I tried this, if the CGWindowNumber is of a window not owned by the calling application, you cannot get the NSWindowEscalade
According to this topic (#31890291), it looks like you can, except that Mr. Dautermann says not all CGWindows are NSWindows, which is true, I think.Fullbodied
Thanks @ElTomato reading now, do you know of how these guys above do it? Do they pretend to be the dock?Escalade
Do you want to know how to inject into code, or something else?kindly clearify your needs.Edlyn
Thanks Sahil. I need to know how to set a the currently active window (even if not owned by process executing the code) to be "always on top". I'll edit the question.Escalade
@SahilDoshi I have cleaned up the question.Escalade
en.wikipedia.org/wiki/SIMBL explains how SIMBL did it: SIMBL loads code via the InputManager system, which was developed to support foreign input methods.Sora
Thanks @TylerLong! I'm not expert on mac docs, I followed the wiki to InputManager and got lost. Do you have any code example specific to my case?Escalade
@Escalade nope. I spent lots of time on this issue but I haven't got it working yet. I can make any windows frontmost. But I failed to make them floating. I even tried AppleScript. I don't think there are any easy solutions. Even SIMBL is having lots of issues with latest macOS.Sora
@Tyler I up voted as thanks for trying so hard! I tried for about a year on and off and haven't been able to knock it out. I'm hoping the +500 bounty will attract some answer. It's badly needed for the open source community, I couldn't find this anywhere.Escalade
@Noitidart,I have a question, which all OS you want to support, because input managers are disabled according to me in latest Mac OS.Edlyn
@Noitidart, have you tried using OSX Accessibility feature?developer.apple.com/library/mac/documentation/Accessibility/…Edlyn
@Noitidart, I don't know how will you achieve your functionality by injecting in other apps. but if you want to inject into other apps, then have you tried github.com/rentzsch/mach_injectEdlyn
I have not yet tried it Sahil, I didn't know if injecting would work.Escalade
Crap :( Bounty is going to expire :(Escalade
Thank you very much all for all the upvotes it helps because my bounty is expiring I wish I could recall the bounty and give it to someone when its solved, but I'll just have to take out another 500 for when someone can help solve it. I thought if no one solves it the bounty came back but apparently it doesn't.Escalade
@TylerLong may you please share your code for making any window frontmost, it might show me a different way to approach things.Escalade
@Escalade I am in office and the code is at home. I will send the code to your email I found on your GitHub profile once I get home. Please remind me if I forget.Sora
Much appreciated brother, thank you!Escalade
@Escalade gist.github.com/tylerlong/b5cee1d57920e705fa2df0b3f0990b48 In order to make it "floating", you can call bringToFront repeatedly. Problem is the window will steal focus repeatedly. I have tried something like setAttribute("Floating", value: true) SetAttribute('Level', 3) it didn't work at all.Sora
Thank you @TylerLong! Very interesting approach.Escalade
A
1

It seems you want to make an external process's window stay on top of all other applications, while the code I provide here does not accomplish exactly what you are looking for, it is at least somewhat similar, and might be good enough for what you need, depending on your use case. In this example I demonstrate how to keep a CGWindowID on top of a specific NSWindow *. Note - the NSWindow * is the parent window, and it will need to be owned by your app, but the CGWindowID used for the child window can belong to any application). If you want the NSWindow * to be the child window, change the NSWindowBelow option to NSWindowAbove.

There is a tiny problem with this solution and that is some minor flickering here and there, when the parent window is attempting to gain focus but then loses it immediately - the flicker happens very quickly and intermittently, perhaps it can be overlooked if you are super desperate.

Anyway, the code is...

cocoa.mm

#import "subclass.h"
#import <Cocoa/Cocoa.h>
#import <sys/types.h>

NSWindow *cocoa_window_from_wid(CGWindowID wid) {
  return [NSApp windowWithWindowNumber:wid];
}

CGWindowID cocoa_wid_from_window(NSWindow *window) {
  return [window windowNumber];
}

bool cocoa_wid_exists(CGWindowID wid) {
  bool result = false;
  const CGWindowLevel kScreensaverWindowLevel = CGWindowLevelForKey(kCGScreenSaverWindowLevelKey);
  CFArrayRef windowArray = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, 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) {
          result = true;
          break;
        }
      }
    }
  }
  CFRelease(windowArray);
  return result;
}

pid_t cocoa_pid_from_wid(CGWindowID wid) {
  pid_t pid;
  const CGWindowLevel kScreensaverWindowLevel = CGWindowLevelForKey(kCGScreenSaverWindowLevelKey);
  CFArrayRef windowArray = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, 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) {
          pid = ownerPID.integerValue;
          break;
        }
      }
    }
  }
  CFRelease(windowArray);
  return pid;
}

unsigned long cocoa_get_wid_or_pid(bool wid) {
  unsigned long result;
  const CGWindowLevel kScreensaverWindowLevel = CGWindowLevelForKey(kCGScreenSaverWindowLevelKey);
  CFArrayRef windowArray = CGWindowListCopyWindowInfo(kCGWindowListOptionAll, 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 == 0) {
        NSNumber *windowID = windowInfoDictionary[(id)kCGWindowNumber];
        result = wid ? windowID.integerValue : ownerPID.integerValue;
        break;
      }
    }
  }
  CFRelease(windowArray);
  return result;
}

void cocoa_wid_to_top(CGWindowID wid) {
  CFIndex appCount = [[[NSWorkspace sharedWorkspace] runningApplications] count];
  for (CFIndex i = 0; i < appCount; i++) {
    NSWorkspace *sharedWS = [NSWorkspace sharedWorkspace];
    NSArray *runningApps = [sharedWS runningApplications];
    NSRunningApplication *currentApp = [runningApps objectAtIndex:i];
    if (cocoa_pid_from_wid(wid) == [currentApp processIdentifier]) {
      NSRunningApplication *appWithPID = currentApp;
      NSUInteger options = NSApplicationActivateAllWindows;
      options |= NSApplicationActivateIgnoringOtherApps;
      [appWithPID activateWithOptions:options];
      break;
    }
  }
}

void cocoa_wid_set_pwid(CGWindowID wid, CGWindowID pwid) {
  [cocoa_window_from_wid(pwid) setChildWindowWithNumber:wid];
}

subclass.mm

#import "subclass.h"
#import <Cocoa/Cocoa.h>

CGWindowID cocoa_wid = kCGNullWindowID;
CGWindowID cocoa_pwid = kCGNullWindowID;

@implementation NSWindow(subclass)

- (void)setChildWindowWithNumber:(CGWindowID)wid {
  [[NSNotificationCenter defaultCenter] addObserver:self
    selector:@selector(windowDidBecomeKey:)
    name:NSWindowDidUpdateNotification object:self];
  cocoa_pwid = [self windowNumber]; cocoa_wid = wid;
  [self orderWindow:NSWindowBelow relativeTo:wid];
}

- (void)windowDidBecomeKey:(NSNotification *)notification {
  if (cocoa_wid_exists(cocoa_wid)) {
    [self setCanHide:NO];
    [self orderWindow:NSWindowBelow relativeTo:cocoa_wid];
  } else {
    cocoa_wid = kCGNullWindowID;
    [self setCanHide:YES];
  }
}

@end

subclass.h

#import <Cocoa/Cocoa.h>

bool cocoa_wid_exists(CGWindowID wid);

@interface NSWindow(subclass)

- (void)setChildWindowWithNumber:(CGWindowID)wid;
- (void)windowDidBecomeKey:(NSNotification *)notification;

@end

I went an extra mile and added some functions to help you retrieve the appropriate CGWindowID based on the frontmost CGWindowID, and if you know the correct CGWindowID beforehand, via AppleScript, or however you prefer, you may bring it to the front using cocoa_wid_to_top(wid), (if the user permits), however this doesn't play well with processes owning multiple visible windows simultaneously, because it brings all windows owned by the process id associated with the given CGWindowID to the top, so you might not have the CGWindowID you wanted to be on the absolute top of the window stack necessarily. The reason you may want the window to be brought on top of the stack is due to the fact there are cases in which a window may open that you would want to make a child window but it appeared on screen underneath your parent window, thus forcing you to click it before the parent/child relationship of windows can effectively take place.

Documentation below...

NSWindow *cocoa_window_from_wid(CGWindowID wid); Returns an NSWindow * from a given CGWindowID, provided the CGWindowID belongs to the current app, otherwise an invalid CGWindowID is returned, which can be represented with the constant kCGNullWindowID.

CGWindowID cocoa_wid_from_window(NSWindow *window); Returns a CGWindowID from a given NSWindow *, provided the NSWindow * belongs to the current app, otherwise I believe you will get a segfault. That's what happens in my testing when you know the value of an NSWindow * and attempt to use it in an app that it doesn't belong to, so don't even try.

bool cocoa_wid_exists(CGWindowID wid); Returns true if the a window based on a specified CGWindowID exists, excluding your screensaver and desktop elements, false if it doesn't.

pid_t cocoa_pid_from_wid(CGWindowID wid); A helper function for cocoa_wid_to_top(wid) which returns the process id, (or pid_t), associated with the given CGWindowID.

unsigned long cocoa_get_wid_or_pid(bool wid); Returns the frontmost CGWindowID if wid is true, otherwise the frontmost process id, (or pid_t), is the result. Note the return type unsigned long can be safely casted to and from a CGWindowID or pid_t as needed.

void cocoa_wid_to_top(CGWindowID wid); Attempts to bring all windows that belong to the process id, (or pid_t), associated with the given CGWindowID to be the topmost app.

Now for the most important function...

void cocoa_wid_set_pwid(CGWindowID wid, CGWindowID pwid); Assigns a parent window based on a specified CGWindowID to the given child window associated with the proper CGWindowID. The parent window id, (or pwid), must be owned by the current app, while the child window id, (or wid), may belong to any application, excluding the screensaver and desktop elements. If the parent or child window ceases to exist, they lose their parent and child relationship to avoid recycled CGWindowID's from inheriting the relationship. If the parent or child CGWindowID doesn't exist, they will be set to kCGNullWindowID, which reliably ends the relationship.

Note this code has been tested in Catalina and indeed works as advertised at the time of writing.

To use the cocoa functions I provided in your C or C++ code you may do this in a header:

typedef void NSWindow;
typedef unsigned long CGWindowID;

extern "C" NSWindow *cocoa_window_from_wid(CGWindowID wid);
extern "C" CGWindowID cocoa_wid_from_window(NSWindow *window);
extern "C" bool cocoa_wid_exists(CGWindowID wid);
extern "C" pid_t cocoa_pid_from_wid(CGWindowID wid);
extern "C" unsigned long cocoa_get_wid_or_pid(bool wid);
extern "C" void cocoa_wid_to_top(CGWindowID wid);
extern "C" void cocoa_wid_set_pwid(CGWindowID wid, CGWindowID pwid);
Animism answered 23/5, 2020 at 20:19 Comment(3)
Thank you for such an in-depth reply! I upvoted it without testing it. Hopefully someone can see this code and get led to the non-owned window trick.Escalade
@Escalade there were some problems with my initial answer which lead to minimized and hidden windows removing the child/parent window relationship but it's now resolved. Turns out cocoa_wid_exists(wid) was returning false incorrectly because that function needed to use the kCGWindowListOptionAll flag internally. That, and the parent window needs to prevent itself from being hidden temporarily long enough for the child window to close, to prevent a segfault; thus the canHide property being set in the subclass.Animism
No problem, I'm using this code in one of my own projects, so it was the least I could do. If you want code that also allows for making the current app the child window, (instead of the parent) have a look here: github.com/time-killer-games/enigma-dev/tree/master/…Animism

© 2022 - 2024 — McMap. All rights reserved.