26

With OSX 10.10 beta 3, Apple released their dark tint option. Unfortunately, it also means that pretty much all status bar icons (with the exception of Apple's and Path Finder's that I've seen), including mine, remain dark on a dark background. How can I provide an alternate image for when dark tint is applied?

I don't see an API change on NSStatusBar or NSStatusItem that shows me a change, I'm assuming it's a notification or something reactive to easily make the change as the user alters the tint.

Current code to draw the image is encased within an NSView:

- (void)drawRect:(NSRect)dirtyRect
{
    // set view background color
    if (self.isActive) {
        [[NSColor selectedMenuItemColor] setFill];
    } else {
        [[NSColor clearColor] setFill];
    }

    NSRectFill(dirtyRect);

    // set image
    NSImage *image = (self.isActive ? self.alternateImage : self.image);
    _imageView.image = image;
}
Vadim Kotov
  • 8,084
  • 8
  • 48
  • 62
Joel Fischer
  • 6,521
  • 5
  • 35
  • 46
  • Are you drawing the image manually in a custom view's `-drawRect:`? Does it help to use an `NSImageView` subview instead? – Ken Thomases Jul 08 '14 at 04:13
  • @KenThomases Should have added current code. It's there now. – Joel Fischer Jul 08 '14 at 04:17
  • 1
    You should use `-drawStatusBarBackgroundInRect:withHighlight:` instead of filling with a color you pick yourself. If doing that doesn't help, you can try setting the background style on the image view. Of course, that raises the question of how to pick an appropriate style. Also, I would move the setting of the image view's image to `-viewWillDraw`. That sort of thing shouldn't be done in `-drawRect:`. – Ken Thomases Jul 08 '14 at 04:35
  • Thanks for the input though i believe you misunderstand. The background goes dark as is appropriate, its the foreground image that remains a dark image on the now dark background. I need to know when to replace it with a white foreground image instead. – Joel Fischer Jul 08 '14 at 04:40
  • My point is that it's very possible that `NSImageView` can tell the difference between a background filled with the colors you've chosen and one drawn by the appropriate method I mentioned. It may change its behavior accordingly. Also, are you using a template image? – Ken Thomases Jul 08 '14 at 06:14
  • I have tried using a template image, but it doesn't alter the color based on the change. It is entirely possible I'm missing a piece of the template API or something though, since I've never used it before. (I'm just setting setTemplate:). – Joel Fischer Jul 08 '14 at 23:26
  • Template images only get special styling when used with views that can provide additional information about that rendering (namely Buttons and Segmented Controls). Just drawing one using -[NSImage drawInRect:] or an NSImageView will not work. – Taylor Jul 09 '14 at 15:33

6 Answers6

63

TL;DR: You don't have to do anything special in Dark Theme. Give NSStatusItem (or NSStatusBarButton) a template image and it will style it correctly in any menubar context.


The reason why some apps' status items (such as PathFinder's) already work in Dark Theme is because they're not setting their own custom view on the StatusItem, but only setting a template image on the StatusItem.

Something like:

_statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
NSImage *image = [NSImage imageNamed:@"statusItemIcon"];
[image setTemplate:YES];
[_statusItem setImage:image];

This works exactly as you'd expect in Mavericks and earlier, as well as Yosemite and any future releases because it allows AppKit to do all of the styling of the image depending on the status item state.

Mavericks

In Mavericks (and earlier) there were only 2 unique styles of the items. Unpressed and Pressed. These two styles pretty much looked purely black and purely white, respectively. (Actually "purely black" isn't entirely correct -- there was a small effect that made them look slightly inset).

Because there were only two possible state, status bar apps could set their own view and easily get the same appearance by just drawing black or white depending on their highlighted state. (But again note that it wasn't purely black, so apps either had to build the effect in the image or be satisfied with a hardly-noticeable out of place icon).

Yosemite

In Yosemite there are at least 32 unique styling of items. Unpressed in Dark Theme is only one of those. There is no practical (or unpractical) way for an app to be able to do their own styling of items and have it look correct in all contexts.

Here are examples of six of those possible stylings:

Six possible status item stylings

Status items on an inactive menubar now have a specific styling, as opposed to a simple opacity change as in the past. Disabled appearance is one other possible variation; there are also other additional dimensions to this matrix of possibilities.

API

Arbitrary views set as NSStatusItem's view property have no way to capture all of these variations, hence it (and other related API) is deprecated in 10.10.

However, seed 3 introduces new API on NSStatusItem:

@property (readonly, strong) NSStatusBarButton *button NS_AVAILABLE_MAC(10_10);

This piece of API has a few purposes:

  1. An app can now get the screen position (or show a popover from) a status item without setting its own custom view.
  2. Removes the need for API like image, title, sendActionOn: on NSStatusItem.
  3. Provides a class for new API: i.e. looksDisabled. This allows apps to get the standard disabled/off styling (like Bluetooth/Time Machine when off) without requiring a custom image.

If there's something that can't be done with the current (non- custom view) API, please file an enhancement request for it. StatusItems should provide behavior or appearances in a way that it standard across all status items.


More discussion is at https://devforums.apple.com/thread/234839, although I've summarized most everything here.

Taylor
  • 3,183
  • 17
  • 18
  • 2
    Note: In seed 3 there is an issue with using `[self.statusItem.button sendActionOn:(NSLeftMouseUpMask|NSRightMouseUpMask)];`, the right click event doesn't fire. There is an open radar here: http://openradar.appspot.com/radar?id=5882037839855616 – Joel Fischer Jul 10 '14 at 03:04
  • 4
    You can also insert a "Template" suffix to the image file name, like "MyIconTemplate.png" and it will work out of the box. Just to mention how beautifully this works, I was able to hack a compiled app by replacing the last 8 chars of the black icon file to "Template" and editing the file reference in the compiled build using a HEX editor. Now it works like a charm! :) – Bruno Philipe Jul 12 '14 at 13:11
  • Currently `image` and `setImage` on NSStatusItem are also deprecated as of 10.10 – mirosval Aug 19 '14 at 10:17
  • 1
    Yep. For 10.10 and later you should just set the image on the StatusItem's button (available on 10.10) – Taylor Aug 20 '14 at 19:02
  • Here is little library that handles menu bar right based on os x version. Can be used as a sample: https://github.com/dmitrynikolaev/MenuBarController – dmitrynikolaev Jan 19 '15 at 09:12
6

I end up did something like following to my custom drag and drop NSStatusItemView: (Using Swift)

var isDark = false

func isDarkMode() {
    isDark = NSAppearance.currentAppearance().name.hasPrefix("NSAppearanceNameVibrantDark")
}

override func drawRect(dirtyRect: NSRect) {
    super.drawRect(dirtyRect)
    isDarkMode()
    // Now use "isDark" to determine the drawing colour.
    if isDark {
        // ...
    } else {
        // ...
    }
}

When the user changed the Theme in System Preferences, the NSView will be called by the system for re-drawing, you can change the icon colour accordingly.

If you wish to adjust other custom UI outside this view, you can either use KVO to observer the isDark key of the view or do it on your own.

Cai
  • 3,609
  • 2
  • 19
  • 39
  • Is drag and drop the only missing behavior from NSStatusItem that requires you to use a custom view? Have you filed a radar requesting that it supports it? – Taylor Jul 09 '14 at 18:02
  • @Taylor for now, yes. And no, I didn't request it. – Cai Jul 09 '14 at 19:13
2

I created a basic wrapper around NSStatusItem that you can use to provide support for 10.10 and earlier with custom views in the status bar. You can find it here: https://github.com/noahsmartin/YosemiteMenuBar The basic idea is to draw the custom view into a NSImage and use this image as a template image for the status bar item. This wrapper also forwards click events to the custom view so they can be handled the same way as pre 10.10. The project contains a basic example of how YosemiteMenuBar can be used with a custom view on the status bar.

Noah
  • 21
  • 1
2

Newest swift code set image template method is here:

// Insert code here to initialize your application
if let button = statusItem.button {
    button.image = NSImage(named: "StatusIcon")
    button.image?.isTemplate = true  // Just add this line
    button.action = #selector(togglePopover(_:))
}

Then it will change the image when dark mode.

zhi.yang
  • 425
  • 5
  • 10
1

When your application has drawn any GUI element you can get its appearance via [NSAppearance currentAppearance] which itself has a name property that holds something like

NSAppearanceNameVibrantDark->NSAppearanceNameAqua->NSAppearanceNameAquaMavericks

The first part is the appearance’s name, which is also available as a constant in NSAppearanceNameVibrantDark or NSAppearanceNameVibrantLight.

I don’t know if there’s a way to get just the first part, but I think this does the trick for now.

Example code:

-(void)awakeFromNib {
    NSStatusItem* myStatusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
    myStatusItem.title = @"Hello World";

    if ([[[NSAppearance currentAppearance] name] containsString:NSAppearanceNameVibrantDark]) {
        myStatusItem.title = @"Dark Interface";
    } else {
        myStatusItem.title = @"Light Interface";
    }
}
max
  • 1,509
  • 1
  • 19
  • 24
  • Thanks for your input. Any ideas on altering based on a change? Is there a notification? I could possibly use KVO, but I think there's a better way than this, since Path Finder already worked as soon as beta 3 released. – Joel Fischer Jul 08 '14 at 23:28
  • This will not automatically adjust if the theme is changed while the status item is running. – Taylor Jul 09 '14 at 00:47
  • @JoelFischer Did you try KVO method yet? I tried with something like `NSAppearance.currentAppearance().addObserver(self,forKeyPath:"name",options: NSKeyValueObservingOptions.Prior,context: nil)` It can detect the change but will cause error `An instance 0x608000465cc0 of class NSCompositeAppearance was deallocated while key value observers were still registered with it.`. Got no idea how to fix that. – Cai Jul 09 '14 at 11:05
  • @x43x61x69 I haven’t tested, but as far as I know it’s not just the name that changes, it’s the whole (current)appearance object (with your KVO on it) that gets replaced. So you’d rather have to observe the currentAppearance instead of just its name. – max Jul 09 '14 at 11:40
1

But just in case you do want to monitor the status changes you can. I also know there is a better way to determine lite/dark mode than what's been said above, but I can remember it right now.

// Monitor menu/dock theme changes...
[[NSDistributedNotificationCenter defaultCenter] addObserver: self selector: @selector(themeChange:) name:@"AppleInterfaceThemeChangedNotification" object: NULL];

//
-(void) themeChange :(NSNotification *) notification
{
    NSLog (@"%@", notification);
}