8
votes

I'm writing a "UIElement" app that shows a status window on the side of the screen, similar to the Dock.

Now, when a program takes over the entire screen, I need to hide my status window, just like the Dock does.

What are my options to detect this and the inverse event?

I like to avoid polling via a timed event and also cannot use undocumented tricks (such as suggested here)

What doesn't work:

  • Registering a Carbon Event Handler for the kEventAppSystemUIModeChanged event isn't sufficient - it works to detect VLC's full screen mode, but not for modern Cocoa apps that use the new fullscreen widget at the top right corner of their windows.

  • Similarly, following Apple's instructions about the NSApplication presentationOptions API by observing changes to the currentSystemPresentationOptions property does not help, either - again, it only informs about VLC's fullscreen mode, but not about apps using the window' top right fullscreen widget.

  • Monitoring changes to the screen configuration using CGDisplayRegisterReconfigurationCallback is not working because there aren't any callbacks for these fullscreen modes.

3
This is an interesting question. I have an idea based around grabbing the onscreen window list and checking whether the desktop is present whenever NSWorkspace's active space change notification fires, but I don't have time to test it out now. Feel free to use that as a jumping off point if you're feeling adventurous and nobody else comes along with something better before I get back to it.Chuck
What is the collectionBehavior of your window? I would think that leaving out NSWindowCollectionBehaviorFullScreenAuxiliary would mean your window was automatically hidden on a full-screen space.Ken Thomases
@KenThomases - it's not my own app I want to observe, it's OTHER apps. My app is a background-only app.Thomas Tempelmann
You said you have a status window, so your app is not background-only. (UIElement is different than background-only.) I was asking about the collectionBehavior of that status window.Ken Thomases
Related with working code too stackoverflow.com/a/15895398/231917zengr

3 Answers

2
votes

Based on @Chuck's suggestion, I've come up with a solution that works somewhat, but may not be foolproof.

The solution is based on the assumption that 10.7's new fullscreen mode for windows moves these windows to a new Screen Space. Therefore, we subscribe to notifications for changes to the active space. In that notification handler, we check the window list to detect whether the menubar is included. If it is not, it probably means that we're in a fullscreen space.

Checking for the presence of the "Menubar" window is the best test I could come up with based on Chuck's idea. I don't like it too much, though, because it makes assumptions on the naming and presence of internally managed windows.

Here's the test code that goes inside AppDelegate.m, which also includes the test for the other app-wide fullscreen mode:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    NSApplication *app = [NSApplication sharedApplication];

    // Observe full screen mode from apps setting SystemUIMode
    // or invoking 'setPresentationOptions'
    [app addObserver:self
          forKeyPath:@"currentSystemPresentationOptions"
             options:NSKeyValueObservingOptionNew
             context:NULL];

    // Observe full screen mode from apps using a separate space
    // (i.e. those providing the fullscreen widget at the right
    // of their window title bar).
    [[[NSWorkspace sharedWorkspace] notificationCenter]
        addObserverForName:NSWorkspaceActiveSpaceDidChangeNotification
        object:NULL queue:NULL
        usingBlock:^(NSNotification *note)
        {
            // The active space changed.
            // Now we need to detect if this is a fullscreen space.
            // Let's look at the windows...
            NSArray *windows = CFBridgingRelease(CGWindowListCopyWindowInfo
                        (kCGWindowListOptionOnScreenOnly, kCGNullWindowID));
            //NSLog(@"active space change: %@", windows);

            // We detect full screen spaces by checking if there's a menubar
            // in the window list.
            // If not, we assume it's in fullscreen mode.
            BOOL hasMenubar = NO;
            for (NSDictionary *d in windows) {
                if ([d[@"kCGWindowOwnerName"] isEqualToString:@"Window Server"]
                 && [d[@"kCGWindowName"] isEqualToString:@"Menubar"]) {
                    hasMenubar = YES;
                    break;
                }
            }
            NSLog(@"fullscreen: %@", hasMenubar ? @"No" : @"Yes");
        }
     ];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    if ([keyPath isEqual:@"currentSystemPresentationOptions"]) {
        NSLog(@"currentSystemPresentationOptions: %@", [change objectForKey:NSKeyValueChangeNewKey]); // a value of 4 indicates fullscreen mode
    }
}
1
votes

Since my earlier answer doesn't work for detecting full screen mode between apps, I did some experimentation. Starting with the solution that Thomas Tempelmann came up with of checking the presence of menu bar, I found a variation that I think could be more reliable.

The problem with checking for the menu bar is that in full screen mode you can move the mouse cursor to the top of the screen to make the menu bar appear, but you're still in full screen mode. I did some crawling through the CGWindow info, and discovered that when I enter full screen, there is window named "Fullscreen Backdrop" owned by the "Dock", and it's not there when not in full screen mode.

This is on Catalina (10.15.6) in an Xcode playground, so it should be tested in a real app, and on Big Sur (or whatever the current OS is, when you're reading this).

Here's the code (in Swift... easier to quickly test something)

func isFullScreen() -> Bool
{
    guard let windows = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) else {
        return false
    }

    for window in windows as NSArray
    {
        guard let winInfo = window as? NSDictionary else { continue }
        
        if winInfo["kCGWindowOwnerName"] as? String == "Dock",
           winInfo["kCGWindowName"] as? String == "Fullscreen Backdrop"
        {
            return true
        }
    }
    
    return false
}
-1
votes

EDIT NOTE: This answer unfortunately doesn't provide a solution for detecting full screen in a different app, which is what the OP was asking. I'm leaving it because it does answer the question for detecting in in the same app going full screen - for example in a generic library that needs to know to automatically update keyEquivalents and title for an explicitly added "Enter Full Screen" menu item rather than Apple's automatically added menu item.

Although this question is quite old now, I've had to detect full screen mode in Swift recently. While it's not as simple as querying some flag in NSWindow, as we would hope for, there is an easy and reliable solution that has been available since macOS 10.7.

Whenever a window is about to enter full screen mode NSWindow sends a willEnterFullScreenNotification notification, and when it is about to exit full screen mode, it sends willExitFullScreenNotification. So you add an observer for those notifications. In the following code, I use them to set a global boolean flag.

import Cocoa

/*
 Since notification closures can be run concurrently, we need to guard against
 races on the Boolean flag.  We could use DispatchSemaphore, but it's kind
 over-kill for guarding a simple read/write to a boolean variable.
 os_unfair_lock is appropriate for nanosecond-level contention.  If the wait
 could be milliseconds or longer, DispatchSemaphore is the thing to use.
 
 This extension is just to make using it easier and safer to use.
 */
extension os_unfair_lock
{
    mutating func withLock<R>(block: () throws -> R) rethrows -> R
    {
        os_unfair_lock_lock(&self)
        defer { os_unfair_lock_unlock(&self) }
        
        return try block()
    }
}

fileprivate var fullScreenLock = os_unfair_lock()
public fileprivate(set) var isFullScreen: Bool = false

// Call this function in the app delegate's applicationDidFinishLaunching method
func initializeFullScreenDetection()
{
    _ = NotificationCenter.default.addObserver(
        forName: NSWindow.willEnterFullScreenNotification,
        object: nil,
        queue: nil)
    { _ in
        fullScreenLock.withLock  { isFullScreen = true }
    }
    _ = NotificationCenter.default.addObserver(
        forName: NSWindow.willExitFullScreenNotification,
        object: nil,
        queue: nil)
    { _ in
        fullScreenLock.withLock  { isFullScreen = false }
    }
}

Since observer closures can be run concurrently, I use os_unfair_lock to guard races on the _isFullScreen property. You could use DispatchSemaphore, though it's a bit heavy weight for just guarding a Boolean flag. Back when the question was first asked, OSSpinLock would have been the equivalent, but it's been deprecated since 10.12.

Just make sure to call initializeFullScreenDetection() in your application delegate's applicationDidFinishLaunching() method.