7

All native controls have different appearance when their parent window is active or inactive. How should we check this state in custom components e.g. while rendering a button cell?

We could inspect controlView.window’s properties like isMainWindow and isKeyWindow, but they don’t cover all cases. For instance, if you open one window of the app on the Desktop and another in a full-screen Space, only one of them can be key or main according to public APIs. However, standard controls seem to render them as active in both Spaces:

Please note how toolbar buttons in both Safari windows are rendered as active. How do we achieve the same behavior?

Vadim
  • 9,383
  • 7
  • 36
  • 58
  • Could you also inspect the windows full screen state? – Mattie Oct 03 '19 at 10:47
  • @Mattie This is still not enough when you switch from the full screen Space to the Desktop – Vadim Oct 03 '19 at 10:58
  • I figured it was a dumb suggestion. How about https://developer.apple.com/documentation/appkit/nswindow/1419707-onactivespace?language=objc – Mattie Oct 03 '19 at 12:25
  • @Mattie I checked this one as well, but it does not work in a split full screen and, again, when you swipe from the full screen Space to the Desktop ‍♂️ – Vadim Oct 03 '19 at 17:40
  • The question is not clear. Is the question about enabling/disabling buttons in toolbar? -> toolbarValidation. Is it about drawing? cell has enabled, highlighted property. It's set automatically and your cell drawing should not observe changes or even look or touch it's window (when drawing). PS: NSAppearance is more about dark, light themes. 3 different topics and even with picture it's not clear. Just my 2cents. PS: There is effectiveAppearance when drawing. – Marek H Oct 07 '19 at 10:42
  • @MarekH The question is about rendering *enabled* toolbar buttons when the parent window is not active e.g. when your focus is in Preferences. It is not about toolbar validation nor about disabled or highlighted states, window appearance is also another topic. – Vadim Oct 08 '19 at 11:16
  • @Vadim use Xcode to attach to Safari and use lldb [NSApp windows] ... to explore what they did. PS: filter windows with correct title. Check source code using hopper... (PrivateFrameworks/Safari.framework) -> it's full of nice objective c code – Marek H Oct 08 '19 at 22:00
  • 1
    @MarekH Not sure what you mean. Debugging active windows is close to impossible because you will want to switch between apps. I bet Safari uses a private API named `NSWindow.hasActiveAppearance`. – Vadim Oct 09 '19 at 11:35
  • @Vadim you can set breakpoint 'br com add -m "-[Class method]" ' and the br com add 1, commands like "po $rdx", even one you use variables. Last command should be "continue" so Safari doesn't hang. Hit DONE and you have nice console output. lldb is really powerful. You can inspect XCode console output later. – Marek H Oct 09 '19 at 14:49
  • @MarekH Disassembly works better, and this still doesn’t help with the original question. It looks like AppKit doesn’t provide any means to detect a visual state of the parent window when you render a control. – Vadim Oct 09 '19 at 20:12
  • @Vadim -How do we achieve the same behavior? -> I gave you hints how to find out what they are doing. If I know the answer I would post it. – Marek H Oct 09 '19 at 22:08
  • 1
    Seems like the `NSWindow.hasActiveAppearance` covers every cases. But the property is not KVC-compliant so we either need to find a private notification for it or use a timer to get updates. – Jonny Oct 20 '19 at 15:15

2 Answers2

2

Fortunately, SwiftUI allows to inherit a new magic property from the Environment:

/// Window state.
@Environment(\.controlActiveState)
var windowState: ControlActiveState

This is an official solution. Cheers!

Vadim
  • 9,383
  • 7
  • 36
  • 58
  • 1
    This does not cover one case, which is 1. make your window active but not full screen; 2. switch to a full screen app; 3. Command-Tab to switch to another app on the same Space that have your active window on it -> the `controlActiveState` stays in `key` state while your window is no longer active. – Jonny Oct 20 '19 at 15:19
0

I figure out this solution in Swift 5 (Use a NSButton instead of NSView)

class SomeView: NSButton {

    init() {
        super.init(frame: NSRect())
        self.isBordered = false

        // Setting `contentTintColor` can trigger `updateLayer()` when window's active state has changed
        self.contentTintColor = .labelColor
    }

    required init?(coder: NSCoder) {
        fatalError()
    }
    
    // Update view's appearance here
    override func updateLayer() {
        super.updateLayer()
        self.layer?.opacity = (self.window?.isMainWindow ?? false) ? 1 : 0.5
    }
}
Alex
  • 124
  • 1
  • 4