Using NSScrollView with CALayer - cocoa

I'm attempting to create a grid style view (similar to NSCollectionView), except using Core Animation. I'm pretty far through it, and the last major thing left to do is implement scrolling.
My setup so far is that I have an NSView subclass (layer backed), and upon initialization it creates and adds the grid layer as a sublayer of the main view layer. I have created a custom CALayoutManager for the grid layer that arranges its subviews in a grid-like formation. As expected, when I add sublayers to the grid layer, the layout manager is called and the layers are positioned automatically. Up to this point, everything is working as it should.
The problem comes when I try to use an NSScrollView as a parent of my custom view to implement scrolling. I set this up as follows: I created my custom view as a subview of the NSScrollView in Interface Builder. Then, in my layout manager class, I added a delegate property and during initialization, my view subclass sets itself as the delegate of the layout manager. At the end of the layoutSublayersForLayer: method of the layout manager, I call upon its delegate with the delegate method layoutManager:contentHeightChanged:. Here is the implementation of that method in my NSView subclass:
- (void)layoutManager:(MyLayoutManager*)manager contentHeightChanged:(CGFloat)height;
{
CGFloat newHeight = [[self enclosingScrollView] contentSize].height;
if (height > newHeight) {
newHeight = height;
}
NSRect newFrame = [self frame];
newFrame.size.height = newHeight;
[self setFrame:newFrame];
}
It's pretty simple, it just checks to see whether the new height is larger than the content size of the scroll view, and sets the views frame with the new height.
This works — to a certain extent. When the view resizes, it sizes the view's frame properly as it should to encapsulate the full height of the content, thereby making scrollbars appear. The problem: the sublayers of the grid layer jitter when the view is being resized with the scrollbars visible. Here's a video showing the problem:
http://vimeo.com/16987653
As you can see, there is no issue when the scrollbars aren't visible (in other words, when the height of the content fits within the bounds of the scroll view). I can confirm that this isn't a problem with the layout manager and dealing with single columns, because I tested the same thing without the scroll view and there are no jitters.
Any advice is greatly appreciated.

Solved this problem by flipping the coordinate system of both the layer and the view (origin at top left corner).

Related

layer-backed NSViews overlapping / order

I set up two NSViews (a toolbar which overlaps view1) with the help of the interface builder / auto layout and then in -(void)awakeFromNib I call
[self.view1 setWantsLayer:YES];
[self.toolbar setWantsLayer:YES];
to make the views layer-backed so I can animate them later on. I found out that if I call setWantsLayer on the toolbar after I call it on view1 it gets displayed over view1 not caring about the order in the interface builder. So far, so good.
My question is: how can I change the order of the views later on in code. Calling
self.toolbar.zPosition = 0;
self.view1.zPosition = 1;
doesn't do anything. Since I'm feeling not so confident with CALayers:
Is there something I'm missing?
And is there a good tutorial for CALayers - I read the docs, but didn't understand everything (concept of layer-hosting views, sublayers, ...)
edit
an image for better understanding (I want to animate the transparency of the green view so the black toolbar is over the red view and the green view is gone. and back to green view):
You need to add the second view relative to the first using addSubview:positioned:relativeTo:. The layer index is relative only to that layers in the parent layer, not the view hierarchy its self.
For example,
[hostingView addSubview:topView
positioned:NSWindowAbove
relativeTo:bottomView];

Centering view in scroll view

I have an NSView embedded inside a NSScrollView.
When changing the magnification in my app I'm changing the view's frame by adjusting the constraints and calling layoutSubtree:.
Now I'd like to center the same area of the view after changing the embedded view's size.
The following bits of code should be doing the trick -
the scrollers actually look updated, i.e. they expand and stick to the same position. However the scroll view doesn't seem to know about the changed position.
When starting to scroll the (updated) view manually the scroll bar positions 'jump' from the position changed via code to the actual position that's reflecting the view's scroll position.
It feels like the inverse of reflectScrolledClipView: is required after setting the scrollers via code..
Pseudocode:
double hScrollPos = NSScrollView.horizontalScroller.doubleValue;
double vScrollPos = NSScrollView.verticalScroller.doubleValue;
[view updateFrameSizeAndStuff];
[view invalidateIntrinsicContentSize];
// this seems to early though:
NSScrollView.horizontalScroller.doubleValue = hScrollPos;
NSScrollView.verticalScroller.doubleValue = vScrollPos;

Shrink NSScroller of NSScrollView in layer-backed view

I'm experiencing a weird problem (Bug?): Say I have a WebView, which will scroll vertically.
I now want to shrink the mainFrame's vertical scroller a little bit, so that its height is smaller than the NSScrollView itself.
The reason for this is that I want to pin two views (on top and on bottom edge) above the webview.
I did that easily in the frameLoad delegate method by altering the verticalScroller's frame (altering origin and height).
It works, but:
However once I set the webView and it's parent NSView to be layer-backed, it stops working, the scroller resets itself to the default position and height.
Now I don't know if this is a bug or not.
Is there any other way I could try to 'inset' the scroller?
Have you tried subclassing NSScroller, setting it as the vertical scroller for your NSScrollView and overriding
-(void) setFrameOrigin:(NSPoint)origin;
-(void) setFrameSize:(NSSize)size;
I assume making the NSView layer-backed causes frameSize to be set again(NSScrollView seems to take quite a bit of control on this because when you create a custom scroller, there is no "clean" way to set an NSScroller as horizontal or vertical other than calling -initWithFrame: with
height > width to automatically set it as vertical
width > height to automatically set it as horizontal
My other suggestion would be, if you want the NSScroller to not fully reflect the size of its parent NSScrollView would you be better off having an NSView as the content view of the webview. And the NSView have 3 subviews. Two views at the top and bottom pinned, and one NSScrollView in the middle. No custom NSScroller necessary in this case.

Cocoa: NSView drawRect painting over IBOutlets

I have an NSView in IB which sits above the app window. I have a subclass of NSView (AddSource) which I assign to the NSView.
On awakeFromNib I instantiate the view:
//add a new Add Source class
addSourceView = [[AddSource alloc] initWithFrame:NSMakeRect(0.0, 959.0, 307.0, 118.0)];
[[winMain contentView] addSubview:addSourceView];
in addSourceView's drawRect method I am adding a white background to the view:
[[NSColor whiteColor] set];
NSRectFill(rect);
[self setNeedsDisplay:YES];//added this to see if it might solve the problem
In winMain's contentView I have a NSButton that when clicked slides the addSourceView onto the window:
NSRect addSourceViewFrame = [addSourceView frame];
addSourceViewFrame.origin.y = 841.0;
[[addSourceView animator] setFrame:addSourceViewFrame];
But it seems as if the app is painting over the IBOutlets I placed on the NSView in IB. If, in IB, I repoistion the NSView so that it is on screen when the app launches everything works fine, the IBOutlets are there as well as the background color.
I'm not sure why this is happening. I've done this before with no problems. I must be doing something different this time.
Thanks for any help.
*note - on the 3rd screen capture, when I say this is what the app looks like when opened, that's when I hard code the Y position of the NSView. When it is functioning correctly it should open as screen capture 1.
Most likely your buttons and custom view are siblings, i.e. they are both subviews of your window's content view. Since siblings are "Stacked" depending on the order in which they are added, when you add the view in code it is being added on top of the buttons. You should be able to fix it by explicitly specifying where the view should be positioned relative to its new siblings like so:
[[winMain contentView] addSubview:addSourceView positioned:NSWindowBelow relativeTo:nil];
which should place it below any existing subviews of your window's content view. Also, remove the setNeedsDisplay: line in drawRect, that leads to unncessary, possibly infinite, redrawing.
EDIT: OK I see what you're doing.
I would suggest creating a standalove view in the NIB by dragging a "Custom View" object into the left hand side (the vertically-aligned archived objects section) and adding your controls there, that should ensure the controls are actualy subviews of the view, then you can just create a reference to the archived view in code, and add/remove it dynamically as needed.
Honestly though, you should probably be using a sheet for these kinds of modal dialogs. Why reinvent the wheel, and make your app uglier in the process?
You added TWO AddSource views to the window. You added one in IB - this view contains your textFields and buttons that are connected to the IBOutlets and it is positioned outside the window.
Then in -awakeFromNib you create another, blank AddSource view (containing nothing) and animate it into the window.
I can't recommend highly enough the Hillegass as the best introduction to IB and the correct way to build Cocoa Apps.
Also, Assertions can be useful to make sure what you think is happening is actually what is happening.
If you are certain you added a button to your view in IB, assert it is so:-
- (void)awakeFromNib {
NSAssert( myButton, #"did i hook up the outlet?");
}
NSAssert is a macro that has zero overhead in a release build.
Calling [self setNeedsDisplay:YES] from -drawRect just causes the same -drawRect to be called again. This will give you big problems.

NSTextField over NSOpenGLView

I have made a window with an NSOpenGLView that I am rendering openGL content into.
I want to add some buttons and text fields to the view: I can add NSTextFields and NSButtons using interface builder (or code) but they do not appear.
NSOpenGLView is documented as not being able to have sub views, so I then made my own CustomGLView by deriving directly from NSView and implementing the code to create and use a NSOpenGLContext in it. But the subviews are still not appearing :- the OpenGL context paints over them.
On Windows this problem does not exist:- Windows used to host OpenGL MUST have the WS_CLIPCHILDREN and WS_CHIPSIBLINGS styles set ensuring that any peer, or sub children (views) will not be obscured by the OpenGL surface.
How do I get subviews to display over a NSView thats drawing using OpenGL ?
You have 2 choices:
Create a window just for the text field. Add as a child window of the one hosting the OpenGL view. Major downside is you have to manage positioning it correctly if the Open GL view is moved.
Set up your view hierarchy like so:
Layer-backed view
Layer-hosting view whose layer contains an OpenGL layer
Text field
Simply call -setWantsLayer:YES on the subviews of the NSOpenGLView.
NSOpenGLView cannot have subviews according to the documentation. Even if you subclass the NSOpenGLView, that will change nothing.
What you can do is to create a NSView that will hold both the NSOpenGLView and the NSTextField. You then overlap them in the right order to make one draw atop the other.
I'm not heavily into OpenGL yet, but it's my understanding that you can accomplish the visual effect of subviews with Quartz Extreme using layer-backed views; however, those may be problematic. Since subviews are not supported directly, any solution is liable to be a hack.
Indeed, the solution in that link actually hacks a second window to appear over your OpenGL display, the second window displaying the Cocoa views you desire.
The following code (from the above link) is something I've not tested (again not being an OpenGL guy by nature -- yet), but appears like a fairly clever approach:
// This is the GL Window and view you already have
glWindow = [[GLWindow alloc] initWithContentRect:windowRect];
glView = [[[GLView alloc] initWithFrame:NSMakeRect(0, 0, windowRect.size.width, windowRect.size.height)] autorelease];
[glView translateOriginToPoint:NSMakePoint(glView.bounds.size.width/2, glView.bounds.size.height/2)];
[glWindow setContentView:glView];
// And here's your transparent UI window
uiWindow = [[TransparentWindow alloc] initWithContentRect:windowRect];
uiView = [[[NSView alloc] initWithFrame:NSMakeRect(0, 0, windowRect.size.width, windowRect.size.height)] autorelease];
[uiView translateOriginToPoint:NSMakePoint(uiView.bounds.size.width/2, uiView.bounds.size.height/2)];
uiView.wantsLayer = YES;
[uiWindow setContentView:uiView];
[glWindow addChildWindow:uiWindow ordered:NSWindowAbove];
Again, I've not tested this, but it looks like it will get you the visual effect you desire.
The text can be rendered into a texture -- I just used this for a project, did a lot of looking for sample code, and ultimately found Apple's GLString demo code, which was an absolute trove of how-to:
http://developer.apple.com/library/mac/#samplecode/CocoaGL/Listings/GLString_m.html
I haven't tried adding buttons, but you can, of course, draw your own and comparing the positions of click events with those of your buttons...
This was my solution:
1) Create a parent NSView (let's call it parentView).
2) Add an NSOpenGLView Child to parentView.
3) Add an additional NSView Child to parentView (make sure this is after the OpenGLView within the hierarchy). You can add additional TextFields, etc. to this view.
4) In the ViewController for the parent make sure you call [parentView setWantsLayer: TRUE]; I did this within -(void) viewWillAppear
1) The NSOpenGLView can have a subview. It can have plenty even.
2) The reason some views, controls and other elements are being bullied by NSOpenGLView is due to the loading process when the Application launches. I.e If you add a slider or textfield above and into the content view of the window where the NSOpenGLView also resides, upon Application-Launch that textfield will most likely wind up beneath the NSOpenGLView.
This is an Apple Bug. And they know about it.
You can solve it quite easily even without adding a subview to NSOpenGLView...
In Interface Builder drag i.e. a CustomView into the canvas (Not the view). And set it the way you want it with sliders, text and what not. Then create an outlet (Call it i.e topView) in your view controller. Then somewhere in your code... Perhaps (applicationDidFinishLaunching) add this line...
[_window.contentView addSubview:_topView];
(Do your positioning & layout)
This will do the exact same thing as if you had dragged it into the contentView yourself inside IB. Only it will draw the darn thing in the correct Z position.
You loose IB's constraints this way and have to it manually
One could also just subclass and CAOpenGLLayer and use that as a backing layer inside of a regular NSView. There too it is drawn correctly...
Here is Apple's way of wanting to do that. CALayers are a Godsend ;)
Enter following String ** NSOpenGLLayer** in search and hit enter to get to where it is...
NSOpenGLLayer
Hope this helps....

Resources