Changing color of the NSWindow titlebar - cocoa

I am developing a desktop application in which I want to change the color of the title bar of an NSWindow. How exactly can I do this?

NSWindow's content view has a superview, which is an instance of NSThemeFrame. That class is responsible for drawing the title text, the window/toolbar background texture, and it contains subviews for everything else (close button, full screen button, NSDocument icon, etc).
You can use the Objective-C runtime to replace NSThemeFrame's drawRect: method with your own method, which will call the parent implementation and then perform custom drawing on top of it.
There is also a private method to find the rect the title is drawn in, and public methods on NSFont to find it's font and font size.
What I did is set the window background colour to be a solid colour (black) instead of a gradient/texture, then set it to be a "textured" window (which causes the background colour to actually be rendered, otherwise it would not happen), then I draw a black square over the title bar in the area where I know the title has already been drawn, then draw my own title in it's place, with light grey instead of dark grey.
Source code is here: https://github.com/abhibeckert/Dux/blob/master/Dux/DuxProjectWindow.m (note: it only does a custom title text colour if DUX_DARK_MODE == 1)
Doing this will probably get your app blocked from the Mac App Store, but it is fairly reliable. Just make sure you test it with every new major version of OS X.

To change the color of the window's toolbar:
Set window style Textured in Attribute inspector.
In code: [window setBackgroundColor: MyCustomColor];

This uses private methods, but works:
NSEnumerator *viewEnum = [[[[[[window contentView] superview] titlebarViewController] view] subviews] objectEnumerator];
NSView *viewObject;
while(viewObject = (NSView *)[viewEnum nextObject]) {
if([viewObject className] == #"NSTextField") [viewObject setTextColor: .. your color .. ];
}

Related

Opt-out borderless NSButton from NSVisualEffectView vibrancy

I have a NSVisualEffectView with vibrancy containing text fields (NSTextField or NSComboBox) and borderless buttons. The buttons are positioned over the text fields and I want to disable the vibrancy effect on the borderless buttons since they're supposed to appear on the white background of the text fields.
What I tried doing, as per recommendation in the NSVisualEffectView class reference, is to wrap my NSButton inside another NSVisualEffectView with its state set to Inactive. What this does is it replaces the "vibrant" background by a light grey background.
The picture below illustrates this. The first field is my attempted solution, the second shows the default behavior of a borderless button as child of a NSVisualEffectView.
I also tried subclassing the NSButton and set its cell background color to white or clear but I always get the grey background.
How can I change the light grey background to a white or clear background?
Thanks
I managed to solve this myself after a few hours of headache. The solution doesn't need the button to be wrapped in a NSVisualEffectView. Simply subclassing NSButton and overriding the allowsVibrancy property and setting it to false was enough.
In Swift:
override var allowsVibrancy: Bool { return false }

Unexpected NSTextField background color, should be transparent

UPDATE:
I've added a sample project for testing, see at the bottom of the post.
ORIGINAL QUESTION:
I've got an NSWindow and I change its background when some other parameters change.
The window background is a gradient I'm drawing by overriding drawRect in a subclass of the window's view.
class MainWindowView: NSView {
override func drawRect(dirtyRect: NSRect) {
var rect = dirtyRect
let gradient = NSGradient(startingColor: backgroundColor, endingColor: darkerBackgroundColor)
gradient.drawInRect(rect, relativeCenterPosition: NSPoint(x: 0, y: 0))
super.drawRect(rect)
}
}
And I've got two NSTextFields on this window.
The NSTextFields are both set to drawsBackground = false in awakeFromNib and set to borderless in IB.
I'm not using NSAttributedStrings here, only changing the stringValue of the NSTextFields, and of course their textColor.
Everything is working... except that sometimes, the text fields have an unexpected slightly dark semi-transparent background.
(It's hard to see on some screens but it's there.)
Question: why does this darker background appear?
And of course: what could I do to fix it?
I'm pretty sure it's the gradient that breaks something but I can't find what...
Note: the project is in Swift but I can read an Objective-C answer.
EDIT:
So indeed it seems to be coming from the gradient that's behind, see this other screenshot from a test window. This time the gradient is drawn in a Custom View under an NSTextView, and the same undesired effect happens: parts of the text field background are visible but shouldn't.
UPDATE:
I have made a very simple example in a project for testing, with a gradient that shows the phenomenon more visibly. There's only a window, my gradient class and a text field. You can get it (30ko only) in this ZIP file.
You always draw the gradient in the dirty rect. When the text changes, that rect is only the size of the textfield, not of the whole view. Your drawRect function then draws the full gradient in the textfield's background rect, rather than just the portion of the background-view-wide gradient you'd see through the textfield.
If you redraw using your view's frame, and ignore the dirty rect argument, you should get the desired appearance.
I'm guessing your text field isn't layer-backed. If not, turn on layers (in IB or via -wantsLayer for the view in code) for at least the text field. If that alone doesn't work, try turning on layers for the gradient-hosting view as well.

Problems with Background: NSButton on NSView

I have a window with a custom view in it. This view get's an image drawn as a background in it. Let's assume for now that it's a structured grey background image.
- (void)drawRect:(NSRect)dirtyRect
{
[super drawRect:dirtyRect];
[self.titlebarBGImage drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}
In this custom view I added a square textured button and configured it in IB to have no border. In my AppDelegate I then give this button an image in the applicationDidFinishLaunching method:
// ...
// [[self.minimizeButton cell] setBackgroundColor:[NSColor clearColor]];
[self.minimizeButton setImage:minimizeImage];
// ...
Somehow the same background (titlebarBGImage) is also drawn as the buttons background, so it has a structured grey colored image, but scaled to fit the button size. This looks silly and is not what I want obviously. If I then uncomment the line with setBackgroundColor: the background is cleared, but the custom views background also doesn't shine through where the buttons image is transparent (the button image is a circle) so I get a rectangle where the windows background shines through (i.e. green in the example image), not the background of the view the button is in.
Here's an image to make it clear what happens:
From top to bottom:
What I want to achieve, i.e. views grey structured bg shines through buttons transparent area
What I get with upper code, i.e. without the uncommented line: The custom views background is drawn squeezed into the buttons background
what I get with the clearColor line, i.e. window color (green) shines through, not the bg of the view.
How can I get the top most result?
Edit: I pushed an example project to github so that you can see what I mean.
One way to solve this is using Core Animation Layers. I found out watching this tutorial. For that you got to your MainMenu.xib file, select the Button object and click on the layers tab in the Utilities (see screenshot (1)). Then activate Core Animation Layer for the top most View. With that all subelements also have Core Animation Layer activated (There might be performance issues with that if the project is bigger, so take care).
Next I subclassed the NSButton (calling it BPButtonView in my Github project which I updated now). Select the Button in the xib-file and change the Custom Class to BPButtonView, or whatever you called it.
The code for the new NSButton subclass looks like this:
#implementation BPButtonView
- (BOOL)wantsUpdateLayer {
return YES;
}
- (void)updateLayer {
if ( [self.cell isHighlighted] ) {
self.layer.contents = [NSImage imageNamed:#"buttonbg.png"];
} else {
self.layer.contents = [NSImage imageNamed:#"buttonbg.png"];
}
}
#end
I used the if-statement in the updateLayer method just to show, that depending of the buttons state (pressed vs. unpressed) you can provide a different picture. In the Video tutorial there's also a nice trick how to achieve a clean resize of a buttons picture.
And here's a screenshot of the resulting app. The red circle in the middle is the button and the green textured thing is the views background.

Custom-drawn NSView Redraws Whole View Inside Edited Subviews

I have an NSView that is drawing a custom background with an image, but whenever I press a button or programmatically edit a label, it seems to draw the background image again inside of the edited subview. I've found that making the view layer-backed in IB solves the issue, but in the larger app I'm creating, making the view layer-backed causes a ton of other issues.
I made this example app to show as clearly as possible what is happening. The second image happens after pressing the button, which programmatically edits the label text. It seems as though the background image is drawing around both the button and label together, starting at the top of the button.
the view is being drawn like this:
- (void) drawRect:(NSRect)dirtyRect {
[[NSImage imageNamed:#"redGreenGradientBG.png"] drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}
Before edit:
After edit:
Is there any way I can fix this without making the view layer-backed?
(Sorry about the gross gradient -- I thought it would illustrate my point clearest)
The dirtyRect parameter that's being passed in to -drawRect: is not the entire bounding rectangle of your view, but rather the rectangle that has been marked as needing an update (ie. the "dirty" rectangle, as the name suggests).
When you press the button or edit the label, its invalidating the display state for only that subview's bounding rectangle and therefore only that rectangle is being passed as dirtyRect. So what you're seeing in those screenshots is the image being drawn into a smaller rectangle inside your view's bounding rectangle.
In your case you should just redraw the entire background in -drawRect: like this (by using self.bounds as the drawing rectangle rather than dirtyRect):
- (void)drawRect:(NSRect)dirtyRect {
[[NSImage imageNamed:#"redGreenGradientBG.png"] drawInRect:self.bounds fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1];
}

Cocoa: [statusItem setView:myView] makes a white bar menu item no matter what

In my small app for Mac OS X I display some info in system menubar. I use
statusItem = [
[[NSStatusBar systemStatusBar]
statusItemWithLength:NSVariableStatusItemLength]
retain
];
It works very nice and I can change the text with
[statusItem setTitle:[NSString stringWithString:#"Woo-hoo"]];
But it uses the default menu font which is too big for my relatively unimportant info. So I decided to reimplement it with a custom view. I created a view in Interface Builder.
Unfortunately, however, when I set it as a view for my menu item with
[statusItem setView:myView];
it just displays a white bar in the menu instead of my thing. I tried to
[statusItem
drawStatusBarBackgroundInRect:[myView frame]
withHighlight:NO];
with no success.
In trying to figure out whether a problem is with the view itself or with the way I assign it to the menubar, I created a window and did
[myTestWindow setContentView:myView];
This one worked seamlessly. This makes me think my view is OK :-)
So, what else can I try to make the menu item display my own view?
Thanks!
It happened to be some weird side-effects of window-view autosizing setup in Interface Builder (let’s call them size-effects). In the Inspector you can setup how subviews get resized upon superview sizing. And so it was somehow broken in my case, such that when window gets small enough (menuitem-high), my elements just got drawn outside of the window’s frame.
I re-configured the sizing in IB, eliminating all the automatics I don’t need, and now it works perfectly: the view from IB gets displayed inside a menu item.
What is the height of the frame of the view? Maybe your view is taller than the menubar and you are drawing outside of it. The current menubar is 22 pixels, but you should ask the systemStatusBar for it's thickness, just in case it ever changes.
Try drawing a frame around your view to see if you are getting anything.
[[NSColor blueColor] set];
NSBezierPath *path = [NSBezierPath bezierPathWithRect:self.bounds];
[path setLineWidth:4.0f];
[path stroke];
If you get just an 'L' shape (the bottom left corner) of blue then the view is too large. If you get a rectangle but still no text then you may not be drawing the text inside the view, look at the coordinates you are drawing the text at (and review View Geometry). Putting the view in a window may have worked because it is larger.
For an example of using text in a status menu view take a look at Matt Gemmell's NSStatusItemTest project.
EDIT:
Sorry, somehow I missed where you said you created the view in IB. I did a quick test and I can see the white box you mentioned.
The docs for NSStatusItem's setView: states
The custom view is responsible for
drawing itself and providing its own
behaviors, such as processing mouse
clicks and sending action messages.
And status item views go into a special (apple private) window called NSStatusBarWindow that may have different internal behavior than normal windows and certainly seems to not support views from IB.
So yes, I think you need to create a custom NSView subclass and do your own drawing in drawrect:.

Resources