简体   繁体   中英

macOS “Big Sur” Detect dark menu-bar/system tray

Starting with macOS (10.16 "Beta"/11.0) "Big Sur", the menu-bar and system tray no longer honor the Desktop dark-mode preference, making it difficult to properly theme a system tray icon for this Desktop.

Previously, using a shell command default read , the dark mode could be detected:

defaults read -g AppleInterfaceStyle
# "Dark"

This still works great for detecting the Window theme, but it does not work for the menu-bar and system tray theme.

Since this area seems to be driven by the wallpaper brightness/whiteness/luminosity, how do we detect a dark system tray?

n

How to detect this in (eg) Objective-C/C++? Any solution is welcome, as most can be adapted.

Question also posted to Apple Developer forums: https://developer.apple.com/forums/thread/652540

Qt5.6 has a feature called setIsMask(...) which allows the OS to handle this automatically. This is effectively an alias for NSImage::setTemplate:Yes

More references to macOS "Dark Mode":

AdoptOpenJDK upstream bug report:

Keywords: NSStatusBar , Menu Bar Extras

I had the same issue but I think I found a solution. As written in AppKit Release Notes for Big Sur (see the entry for NSStatusItem ) you can just observe NSStatusItem 's button 's effectiveAppearance . If the name of the effectiveAppearance contains dark , then it's the dark mode. Otherwise it's the light mode.

A sample code I created which shows light or dark as the text label of the NSStatusItem is available at this GitHub repo , see in particular AppDelegate.m . (I'm sorry for being a near-extinct dinosaur for using Objective-C.) You can test it by running on Catalina or on Big Sur, changing the Dark/Light settings or the color of the desktop picture from System Preferences.

Edit: It turns out that Big Sur sometimes changes effectiveAppearance from light to light or from dark to dark (in the sense that KVO is invoked although the appearance hasn't actually changed.) Therefore it's advisable to check both the value of effectiveApparance before and after the change to confirm the value actually changed.

I just submitted a TSI and I got an answer:

But I would not add an NSView on top of the NSStatusItem's button directly. The docs mention to use the button property to customize the appearance and behavior of the status item, but it should work within the confines of the button itself, that is, it's various properties like the image and text and their placements. Years ago, NSStatusItem allowed for custom views, but then became deprecated, in order to support a button-based UI, therefore allowing its drawing behavior to easily adapt to changes in the menu bar's appearance.

So unfortunately there is no way to get this information programatically. However, getting information is very important for three of my apps, so I went exploring.

For me personally, it was very important that getting this information will not trigger any security prompts.

I came up with the following idea:

  • Create and hide a NSStatusItem
  • Set a templated NSImage
  • Render the contents of the CALayer into a NSImage

You can use this code to get the colour information (note: the NSStatusItem is never visible and does not cause existing items to move or something like that). Feel free to adjust the formatting and classes:

I have created a class called MenuBar with a public property:

public class MenuBar {
    private static var statusItem: NSStatusItem?

    public static var theme: MenuBarTheme {
        if self.statusItem == nil {
            self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
            self.statusItem?.button?.image = NSImage(systemSymbolName: "circle.fill", accessibilityDescription: nil)
            self.statusItem?.isVisible = false
        }
        
        if let color = self.getPixelColor() {
            return color.redComponent < 0.20 && color.blueComponent < 0.20 && color.greenComponent < 0.20 ? .light : .dark
        }
        else
        {
            return NSApplication.isDarkMode ? .dark : .light
        }
    }

    public static var tintColor: NSColor {
        return self.theme == .light ? NSColor.black : NSColor.white
  }
    
  // MARK: - Helper
  fileprivate static func getPixelColor() -> NSColor?
  {
     if let image = self.statusItem?.button?.layer?.getBitmapImage() {
        let imageRep = NSBitmapImageRep(data: image.tiffRepresentation!)
            
         if let color = imageRep?.colorAt(x: Int(image.size.width / 2.0), y: Int(image.size.height / 2.0)) {
            return color
         }
     }
        
     return nil
  }
}

public enum MenuBarTheme : String
{
    case light = "light"
    case dark = "dark"
}

public extension NSApplication
{
    class var isDarkMode: Bool
    {
        return NSApplication.shared.appearance?.description.lowercased().contains("dark") ?? false
    }
}

public extension CALayer
{
    func getBitmapImage() -> NSImage
    {
        let btmpImgRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(self.frame.width), pixelsHigh: Int(self.frame.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 32)

        let ctx = NSGraphicsContext(bitmapImageRep: btmpImgRep!)
        let cgContext = ctx!.cgContext
        
        self.render(in: cgContext)
        
        let cgImage = cgContext.makeImage()

        return NSImage(cgImage: cgImage!, size: CGSize(width: self.frame.width, height: self.frame.height))
    }
}

For Java 17, this feature has been added. It will eventually be backported to Java 11.

https://github.com/openjdk/jdk/pull/481

The new property is:

apple.awt.enableTemplateImages

From command line:

  • -Dapple.awt.enableTemplateImages=true

... or via Java:

  • System.setProperty("apple.awt.enableTemplateImages", "true");

Note, staring with Big Sur, icons aren't purely black or purely white anymore (but rather a slight shade off), so using the template API is important for proper look & feel.

Although newer JDK versions will have this setTemplate feature natively , for older Java versions, this technique uses JNA and depends on the https://github.com/dyorgio/macos-tray-icon-fixer project. Internally, it uses a combination of Java reflection and JNA pointers to update the image and set the setTemplate flag to true .

It has been tested with JDK8 and JDK11. It will work with new Apple Silicon Macs as well if using a port of JNA with aarch64 support .

Thanks to @dyorgio for the trick.

// Verify theme to choose image version
Image iconImage = MacOSTrayIconFixer.getInitialIcon(blackImage, whiteImage);
// Create your TrayIcon using image
TrayIcon icon = new TrayIcon(iconImage, "Test");
// (Optional) add action and menu
icon.addActionListener((e) -> System.out.println("examples.BasicUsage.main():" + SwingUtilities.isEventDispatchThread()));

// Include in SystemTray BEFORE call fixer
SystemTray.getSystemTray().add(icon);

// Fix
MacOSTrayIconFixer.fix(icon, blackImage, whiteImage, false, 0);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM