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;
}
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:
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:
- An app can now get the screen position (or show a popover from) a status item without setting its own custom view.
- Removes the need for API like
image
, title
, sendActionOn:
on NSStatusItem.
- 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.
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.
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.
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";
}
}
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);
}
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.