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:
I come across CGSSetWindowLevel
in CGPrivate.h
- undocumented stuff - https://gist.github.com/Noitidart/3664c5c2059c9aa6779f#file-cgsprivate-h-L63 - However I recall I tried something like this in the past but would get an error as I tried to connect to a window that wasn't in the calling process.
It says here - https://github.com/lipidity/CLIMac/blob/114dfee39d24809f62ccb000ea22dfda15c11ce8/src/CGS/CGSInternal/.svn/text-base/CGSConnection.h.svn-base#L82
Only the owner of a window can manipulate it. So, Apple has the concept of a universal owner that owns all windows and can manipulate them all. There can only be one universal owner at a time (the Dock).
Maybe, is there anyway to pretend for my calling process to temporarily be the dock? Maybe CGSGetConnectionIDForPSN
for the dock then use that connection?
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.
Here, check that “Displays have separate Spaces” is active, then open some apps. With the toolbar of one app, hover over the green window button. You'll see a pop-up asking you to choose a tile format. Once you choose one, it will be replicated within your Spaces.
Click the Apple () symbol in the menu bar and select System Preferences... in the dropdown menu. Click the Dock & Menu Bar icon in the preference pane. Make sure Dock & Menu Bar is selected in the sidebar, and under "Menu Bar," uncheck the box next to Automatically hide and show the menu bar in full screen.
In the Safari app on your Mac, drag the tab with the website you want to pin to the left side of the tab bar. When the tab shrinks and displays the website's icon, or the pin appears, drop the tab in place. You can also Control-click a tab, then choose Pin Tab.
Click and hold the full-screen button in the upper-left corner of a window. As you hold the button, the window shrinks and you can drag it to the left or right side of the screen. Release the button, then click a window on the other side of the screen to begin using both windows side by side.
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With