Custom-drawn NSView Redraws Whole View Inside Edited Subviews - macos

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

Related

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.

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.

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.

NSButtons leave artefacts when used as subviews of a custom toolbar view

I'm placing a few buttons in a simple rectangular NSview which acts as a custom toolbar. On first render the buttons/views come out as expected, but every time a button is pressed (and sometimes with no mouse interaction at all) artefacts start appearing.
Before
After
I can eliminate the artefacts by calling a [self.toolbarView setNeedsDisplay:YES] in all the action and focus methods but this seems like a hack, is there any clean way to deal with this?
It was a beginner's problem. In the drawRect method
- (void)drawRect:(NSRect)dirtyRect
I was using the param dirtyRect for drawing an outline of my view, assuming it was the view's bounds, where in fact it was only the area around the buttons that became dirty when they were pressed. The 'artefacts' were actually my outline being drawn in the wrong place.
By correctly using the bounds of the view
NSRect drawingRect = [self bounds];
the 'artefacts' no longer appeared.
You just try to set focus ring for a buttons to 'none' in IB.

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.

Resources