How to interact with layer-backed views on the Mac - objective-c

I am designing a user interface containing several labels and text fields. I would like to style the UI like this:
setting a background pattern for the content view of my NSWindow
adding a custom icon to the background in the upper left corner
I solved the first problem by making the content view a layer-backed view as described in Apple's documentation of NSView:
A layer-backed view is a view that is backed by a Core Animation layer. Any drawing done by the view is the cached in the backing layer. You configured a layer-backed view by simply invoking setWantsLayer: with a value of YES. The view class will automatically create the a backing layer for you, and you use the view class’s drawing mechanisms. When using layer-backed views you should never interact directly with the layer.
A layer-hosting view is a view that contains a Core Animation layer that you intend to manipulate directly. You create a layer-hosting view by instantiating an instance of a Core Animation layer class and setting that layer using the view’s setLayer: method. After doing so, you then invoke setWantsLayer: with a value of YES. When using a layer-hosting view you should not rely on the view for drawing, nor should you add subviews to the layer-hosting view.
and then generating a CGColorRef out of a CGPattern which draws my CGImage:
NSView *mainView = [[self window]contentView];
[mainView setWantsLayer:YES];
To set the background image as a pattern I used the answer from How to tile the contents of a CALayer here on SO to get the first task done.
However for the second task, adding the icon I used the code below:
CGImageRef iconImage = NULL;
NSString *path = [[NSBundle mainBundle] pathForResource:#"icon_128" ofType:#"png"];
if(path != nil) {
NSURL *imageURL = [NSURL fileURLWithPath:path];
provider = CGDataProviderCreateWithURL((CFURLRef)imageURL);
iconImage = CGImageCreateWithPNGDataProvider(provider,NULL,FALSE,kCGRenderingIntentDefault);
CFRelease(provider);
}
CALayer *iconLayer = [[CALayer alloc] init];
// layer is the mainView's layer
CGRect layerFrame = layer.frame;
CGFloat iconWidth = 128.f;
iconLayer.frame = CGRectMake(0.f, CGRectGetHeight(layerFrame)-iconWidth, 128.f, 128.f);
iconLayer.contents = (id)iconImage;
CGImageRelease(iconImage);
[layer insertSublayer:iconLayer atIndex:0];
[iconLayer release];
The Questions
I am not sure if I am violating Apple's restrictions concerning layer-backed views that you should never interact directly with the layer. When setting the layer's background color I am interacting directly with the layer or am I mistaken here?
I have a bad feeling about interacting with the layer hierarchy of a layer-backed view directly and inserting a new layer like I did for my second task. Is this possible or also violating Apple's guidelines? I want to point out that this content view of course has several subviews such as labels, a text view and buttons.
It seems to me that just using one single layer-hosting NSView seems to be the cleanest solution. All the text labels could then be added as CATextLayers etc. However if I understand Apple's documentation correctly I cannot add any controls to the view anymore. Would I have to code all the controls myself in custom CALayers to get it working? Sounds like reinventing the wheel de luxe. I also have no idea how one would code a NSTextField solely in CoreAnimation.
Any advice on how split designing user interfaces with CoreAnimation and standard controls is appreciated.
Please note that I am talking about the Mac here.

no layer backing needed IMHO:
for 1. I do a pattern image
NSImage *patternImage = [NSImage imageNamed:#"pattern"];
[window setBackgroungdColor:[NSColor colorWithPatternImage:patternImage]];
for 2. add an NSImageView as a subview of the contentview
NSImageView *v = ...
[[window contentView] addSubview:v];
on mac some views dont respond nicely IF layer backed
:: e.g. pdfview

Make a superview container A. Add a subview B to A for all your NSView needs (buttons, etc.). Add a subview C to A for all your Core Animation needs.
Edit:
Even better: use superview A for all your NSView needs and one subview C for your Core Animation needs, ignoring view B altogether.

Related

Adding NSView to window not on top?

I am adding a subview programmatically and adding it to the main windows context view to cover up the entire context view like so:
loadingView = [[LoadingView alloc] initWithFrame:[mainWindow.contentView frame]];
NSLog(#"%#", [mainWindow.contentView subviews]);
[mainWindow.contentView addSubview:loadingView];
NSLog(#"%#", [mainWindow.contentView subviews]);
[mainWindow makeFirstResponder:loadingView];
The NSLog's confirm that loadingView is being added last in the contentView subviews. I have also tried:
loadingView = [[LoadingView alloc] initWithFrame:[mainWindow.contentView frame]];
[mainWindow.contentView addSubview:loadingView positioned:NSWindowAbove relativeTo:nil];
[mainWindow makeFirstResponder:loadingView];
That didn't work either. For some reason the two tableviews (created in IB) at the bottom of the window are on top of the new view I've added. Here's a snapshot of the window, note that the red part is what should be on top with the progress bar and a few labels:
It's also worth noting that the view has it's alpha set to 0.9 which is why you can somewhat see behind it.
GW
If you place one view above another, the objects in the previous view will be visible in above view. What you need do is remove previous views from window and then add a new subview.
Try using:
//Create IBOutlet of your tableview in your .h file
IBOutlet NSTableView* yourTableView;
// Add this line where you are adding your subview to remove the tableview from superview.
[yourTableView removeFromSuperview];
// Then add your loading view as the subview
loadingView = [[LoadingView alloc] initWithFrame:[mainWindow.contentView frame]];
[mainWindow.contentView addSubview:loadingView];
Then whenever your want your tableView back use:
[window contentView]addSubview: yourTableView];
As long as you use nil here, you will not get predictable results.
[mainWindow.contentView addSubview:loadingView positioned:NSWindowAbove relativeTo:nil];
If you have not already done so, put all the other views inside a containing view.
Make the containing view the only view that is a direct child of the window content view.
Now add your subview with the above method and replace nil with a reference to the containing view.
In OS X, overlapping siblings have certain nuances when it comes to drawing. In your case, the loadingView and the two table views are siblings because they are all added as subviews of the window's content view and they overlap hence the nuances are coming into play.
From Apple's Documentation
For performance reasons, Cocoa does not enforce clipping among sibling
views or guarantee correct invalidation and drawing behavior when
sibling views overlap. If you want a view to be drawn in front of
another view, you should make the front view a subview (or descendant)
of the rear view.
I don't have the definitive solution for this but reading these should help improve your understanding for the long term.
http://www.cocoabuilder.com/archive/cocoa/327817-overlapping-sibling-views.html
Is there a proper way to handle overlapping NSView siblings?

How to add a CALayer to an NSView on Mac OS X

I'm trying to learn how to use and implement CALayer in a Mac Objective-C application, but I can't seem to probably do the most basic thing - add a new layer and set its background colour/frame size. Can anyone see what is wrong with my code?
CALayer *layer = [CALayer layer];
[layer setFrame:CGRectMake(0, 0, 100, 100)];
[layer setBackgroundColor:CGColorCreateGenericRGB(1.0, 0.0, 0.0, 1.0)];
[self.layer addSublayer:layer];
[layer display];
I put this in the - (void)drawRect:(NSRect)rect method of my custom NSView subclass, but when I run the application, it just shows a blank view, with no background colour or evidence of the layer I created.
First of all, you don't want to add a layer in the drawRect: method of a view, this gets called automatically by the system and you'd probably end up with a lot more layers than you actually want. initWithFrame: or initWithCoder: (for views that are in a nib file) are better places to initialize your layer hierarchy.
Furthermore, NSViews don't have a root layer by default (this is quite different from UIView on iOS). There are basically two kinds of NSViews that use a layer: layer-backed views and layer-hosting views. If you want to interact with the layer directly (add sublayers etc.), you need to create a layer-hosting view.
To do that, create a CALayer and call the view's setLayer: method. Afterwards, call setWantsLayer:. The order is important, if you'd call setWantsLayer: first, you'd actually create a layer-backed view.
You need to make a call to the "setWantsLayer" method.
Check out the following documentation for the description for setWantsLayer:
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/Reference/NSView.html
In a nutshell, your view needs to be layer-hosting view. Because it is a layer-hosting view, you should interact with the layer, and NOT interact with the view itself and don't add subviews to it.
[self setLayer:[CALayer new]];
[self setWantsLayer:YES]; // the order of setLayer and setWantsLayer is crucial!
[self.layer setBackgroundColor:[backgroundColor CGColor]];
Put this out of the drawRect. I normally put my layer setup in either the init method or the viewDidLoad.
Otherwise anytime the view is drawn a new layer is added and allocated. Also I've never used the [layer display] line before. The docs actually tell you not to call this method directly.
Updated info (Swift): first call view.makeBackingLayer() then set wantsLayer to true.
https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer

Blurred background underneath an NSView

I've been looking around but I can't find an answer on this. I'm trying to make a view that will display it's contents normally, but anything that's underneath it (in the z axis) would be blurred.
I can't seem to manage to do this right off the bat. Anything I try (at best) blurs the contents of the view, not what's underneath.
Check out this article on Cocoanetics, it gives you some instructions how to properly set up a NSView with blurred background: http://www.cocoanetics.com/2013/10/blurring-views-on-mac/
There's also a RMBlurredView class on Github: https://github.com/raffael/RMBlurredView
The idea behind it is to use a layer-backed NSView with a CIGaussianBlur CIFilter applied on the backgroundFilters property. All important flags are set correctly in the mentioned class.
If you use an image that is significantly smaller than the view's frame and scale the image up, it will be blurred.
You could use CALayers. Declare a root layer and add the background image as a sublayer. Put the "contents" in a subview with a transparent background. (Otherwise the "contents" would probably be obscured by the blurred sublayer.)
Here's partial (and untested) code for setting up the sublayer:
// Set up the root layer.
[[self.aViewController view] setLayer:[CALayer layer]];
[[self.aViewController view] setWantsLayer:YES];
// Set up a sublayer.
CALayer *blurredSublayer = [CALayer layer];
NSRect rectOfView = [self.aViewController view] frame];
blurredSublayer.bounds = rectOfView;
// Views are usually positioned from their lower left corner, but CALayers are positioned from their center point.
blurredSublayer.position = CGPointMake(rectOfView.size.width/2, rectOfView.size.height/2);
// Set the sublayer's contents property to the image you've chosen. You might need to do the scaling when you set up the CGImageRef used by the contents property. (I haven't done this; I leave it to you.)
// Add the sublayer to the view's root layer.
[self.aViewController.view.layer addSublayer:blurredSublayer];
// You'll probably want to save expense by calling setWantsLayer:NO for the contents-bearing subview, since it will have been turned on when you set up the superview's root layer.
Or it might be easier to use a CGContext. But with the CALayer you have the the zPosition property you mentioned.
P.S. The use of "contents" and "superview" and "sublayer" make the structure confusing. Here's a descriptive hierarchy. The first-named item is on the bottom and the last-named on top, as it would be in IB:
superview with root layer
superview's blurred sublayer (sublayer's contents property is scaled-up image)
subview/s (textfields or whatever you had in mind as "contents")

How to add a blurred view ontop of a view?

I have an NSTableView that gets reloaded. While new data is loading, I want to add a subview ontop of it with a spinner. I would like the view ontop to be semi-transparent and reveal the view beneath it, to be blurred. How would I go about doing this?
The easiest solution—significantly more so than the -bitmapImageRepEtc: one, and more applicable to Mac OS than the rasterization-scale method—is to set your overlay view to use a Core Animation backing layer, then give that layer a Core Image blur filter. It's a technique used all over the Mac OS, from the Dock menus to the menu bar itself. Interface Builder makes it trivially easy to set up, but you can do it in code as well, like this:
CALayer *backgroundLayer = [CALayer layer];
[backgroundView setLayer:backgroundLayer];
[backgroundView setWantsLayer:YES];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[backgroundView layer].backgroundFilters = [NSArray arrayWithObject:blurFilter];
You should check out RMBlurredView on guthub: https://github.com/raffael/RMBlurredView
It's an easy to use subclass of NSView that does all that for you. Be sure to set setWantsLayer:YES on your parent view!
For details, check out Cocoanetics article: http://www.cocoanetics.com/2013/10/blurring-views-on-mac/
The basic technique would be to snap an image of your view, using something like the ‑bitmapImageRepForCachingDisplayInRect: method of NSView, processing that image to make it blurred (Core Image is your friend here) and then overlay your view with an NSImageView containing the blurred image.
This is fakery, of course, but that's what showmanship is about :-)
Have you tried changing the Alpha attribute for the view (for transparency)?
Also here's a link on blurring a view:
Blur Effect for UIView

Draw an NSView into an NSGraphicsContext?

I have a CGContext, which I can turn into an NSGraphicsContext.
I have an NSWindow with a clipRect for the context.
I want to put a scrollview into the context and then some other view into the scrollview so I can put an image into it... However, I can't figure out how to attach the scrollview into the context.
Eventually the view will probably be coming from a nib, but I don't see how that would matter.
I've seen this thread, (http://lists.apple.com/archives/quartz-dev/2006/Nov/msg00010.html) But they seem to leave off the step of how to attach the view into the context, unless there's something obvious I'm missing.
EDIT:
The reason I'm in this situation is that I'm writing a Mozilla Plugin. The browser gives me a CGContext (Quartz) and a WindowRef (QuickDraw). I can turn the CGContext into an NSGraphicsContext, and I can turn the windowRef into an NSWindow. From another data structure I also have the clipping rectangle...
I'm trying to draw an image into that context, with scrollbars as needed, and buttons and other UI elements... so I need (want) an NSView...
You can't put a view into a graphics context. A view goes either into another view, or as the content view of a window.
You can draw a view into a context by setting that context as the current context and telling the view to draw. You might do this as a means of rendering the view to an image, but otherwise, I can't think of a reason to do it. (Edit: OK, being a Netscape plug-in is probably a good reason.)
Normally, a view gets its own graphics context in NSView's implementation of the lockFocus method, which is called for you by display, which is called for you by displayIfNeeded (only if the view needs display, obviously), which is called for you as part of the event loop.
You don't need to create a context for a view except in very rare circumstances, such as the export-to-an-image case I mentioned. Normally, you let the view take care of that itself.
A partial solution?
What I have done currently is create a nib with a button in an IKImageView inside an NSScrollView. I load this in my plugin.
Then, since I have the NSWindow, I can get the contentView of the window. Then, I add the scrollview as subview of contentView.
It appears, but there seems to be some coordinate confusion about where the origin is. (top vs bottom) and since I'm mucking with the contentview of the WHOLE WINDOW, I'm doing some stuff very globally that perhaps I should be doing more locally. Like, the view never disappears, even when you close the tab, or go to another tab. (it does close when you close the window of course)
So, does this sound like a reasonable way of doing this? it feels a bit ... kludgy...
For future generations (and me when I forget how I did this and Google leads me back to my own question) Here's how I'm doing this:
I have a NIB with all my views, I load this on start-up.
on SetWindow, I set the clip rect and actually do the attaching:
NP_CGContext* npContext = (NP_CGContext*) window->window;
NSWindow* browserWindow = [[[NSWindow alloc] initWithWindowRef:npContext->window] autorelease];
NSView* cView = [browserWindow contentView];
NSView* hitView = [cView hitTest:NSMakePoint(window->x + 1, clip.origin.y + 1)];
if (hitView == nil || ![[hitView className] isEqualToString:#"ChildView"])
{
return;
}
superView = [hitView retain];
[superView addSubview: topView];
[superView setNextResponder: topView];
[topView setNextResponder: nil];
[browserWindow makeFirstResponder: topView];
To make sure I only addSubView once, I have a flag...
And then in handleEvent, I actually draw, Because I'm using an IKImageView, I can use the undocumented method: [imageView setImage: image]; which takes an NSImage.
So far this seems to be working for me. Hopefully this helps someone else.