Problems with Background: NSButton on NSView - cocoa

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.

Related

How do I make a button in a custom NSView change color on mouseover?

This is for a Mac app written with Cocoa and Objective-C.
I have a custom NSView class that essentially works as a collection of buttons and stores the value of the selected button. Sort of like an NSSlider that snaps to the tick marks but with buttons instead of a slider. The image below on the left is what it looks like.
Now what I want to do is make it so that when the mouse moves over each button, it covers that button with a semi-transparent blue color that then stays there when it is clicked. I've made a few attempts and you can see the latest result in the image on the right:
This is what happens after mousing over all the buttons. For some reason it draws using the window's origin instead of drawing inside the MyButtonView. Also, it is not semi-transparent. I haven't yet worried about redrawing the normal button when the mouse leaves the rectangle since this part isn't working yet anyway.
Now here's the pertinent code.
Inside the initWithFrame method of the MyButtonView class:
for (int i = 0; i < 12; i++) {
yOrigin = kBorderSize + (buttonHeight * i) + (kSeparatorSize * i);
NSRect newRect = { {xOrigin, yOrigin}, {buttonWidth, buttonHeight} };
[buttonRectangles addObject:NSStringFromRect(newRect)];
[self addTrackingRect:newRect owner:self userData:NULL assumeInside:NO];
}
The methods that draw the blue rectangles:
- (void)mouseEntered:(NSEvent *)theEvent {
NSRect rect = [[theEvent trackingArea] rect];
[self drawHoverRect:rect withColor:hoverBlue];
}
- (void)drawHoverRect:(NSRect)rect withColor:(NSColor *)color {
[color set];
NSRectFill(rect);
[self displayRect:rect];
}
I have no idea how to do this. I've been poring over Apple's documentation for a few hours and can't figure it out. Obviously though, I'm no veteran to Cocoa or Objective-C so I would love some help.
One fundamental problem you have is that you are bypassing the normal drawing mechanisms and trying to force the drawing yourself. This is a common mistake for first timers. Before you go any further, you should read Apple's View Programming Guide:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaViewsGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40002978
If you have trouble with that, then you might need to back up and start with some of the more fundamental Objective-C/Cocoa guides and documentation.
Back to your actual view, one thing that you are going to have to do in this view is do all your drawing in the drawRect: method. You should be tracking the state of your mouse movements via some kind of data structure and then drawing according to that data structure in your drawRect: method. You will call
[self setNeedsDisplay:YES];
in your mouse tracking method(s), after you've recorded whatever change has occurred in your data structure. If you only want to ever draw one button highlighted at a time, then your data structure could be as simple as an NSInteger whose value you set to the index of your selected button (or -1 or whatever to indicate no selection).
For the sake of learning, the reason your blue boxes are currently drawing from the window's origin is that you are calling drawing code outside of the "context" that's normally setup for your view when drawRect: is called by the system. That "context" would include a translation to move the current origin to the origin of your view, rather than the origin of the window.

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];
}

NSScrollView as subview of layer-backed view bug?

When I make an NSScrollView a subview of a layer-backed view (for example by choosing an NSTextView or NSTableView from IB), I see strange drawing behavior in the scroll view's document view.
To illustrate, I created a simple project with an NSTextView in a window. The only code I wrote is to turn on layer-backing for the window's content view:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[[self.window contentView] setWantsLayer:YES];
}
This is the result when I type into the textview. The red underlines don't line up properly:
http://mowglii.com/random/screenshot.png
Also, the text and underlines jitter a lot when I resize the textview. I've seen the same jitter on resize when I use a tableview (inside a scrollview) instead of a textview.
Any idea what is going on?
NSScrollView indeed misbehaves badly when embedded in a layer-backed view.
Some serious trickery is needed to animate views containing scroll views. You could try turning on layer-backing only once you need to animate. You then need to force the drawing in order not to end up with an empty layer.
Usually, you will however need to go look much deeper in the back of tricks. Namely: Keep layer-backing switched off. Draw the view into an image, display that image in an overlay view with layer-backing enabled. Animate that view to an image of the final state. Then remove the overlay view to reveal the actual final state below.

Changing color of the NSWindow titlebar

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 .. ];
}

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