Files
twui/lib/UIKit/TUIViewNSViewContainer.m

331 lines
8.4 KiB
Objective-C

/*
Copyright 2011 Twitter, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this work except in compliance with the License.
You may obtain a copy of the License in the LICENSE file, or at:
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Portions of this code were taken from Velvet,
// which is copyright (c) 2012 Bitswift, Inc.
// See LICENSE.txt for more information.
#import "TUIViewNSViewContainer.h"
#import "CATransaction+TUIExtensions.h"
#import "TUINSView.h"
#import "TUINSView+Private.h"
#import "TUIViewNSViewContainer+Private.h"
#import <CoreServices/CoreServices.h>
@interface TUIViewNSViewContainer () {
/**
* A count indicating how many nested calls to <startRenderingContainedView>
* are in effect.
*/
NSUInteger _renderingContainedViewCount;
}
- (void)synchronizeNSViewAppearance;
- (void)startRenderingContainedView;
- (void)stopRenderingContainedView;
@end
@implementation TUIViewNSViewContainer
#pragma mark Properties
@dynamic hostView;
@synthesize rootView = _rootView;
- (void)setRootView:(NSView *)view {
NSAssert1([NSThread isMainThread], @"%s should only be called from the main thread", __func__);
// remove any existing guest view
[_rootView removeFromSuperview];
_rootView.hostView = nil;
_rootView = view;
TUINSView *nsView = self.ancestorTUINSView;
// and set up our new view
if (_rootView) {
// set up layer-backing on the view
[_rootView setWantsLayer:YES];
[_rootView setNeedsDisplay:YES];
[nsView.appKitHostView addSubview:_rootView];
_rootView.hostView = self;
[nsView recalculateNSViewOrdering];
_rootView.nextResponder = self;
[self synchronizeNSViewAppearance];
} else {
// remove the old view from the TUINSView's clipping path
[nsView recalculateNSViewClipping];
}
}
- (CGRect)NSViewFrame; {
// we use 'self' and 'bounds' here instead of the superview and frame
// because the superview may be a TUIScrollView, and accessing it directly
// will skip over the CAScrollLayer that's in the hierarchy
return [self convertRect:self.bounds toView:self.ancestorTUINSView.rootView];
}
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self synchronizeNSViewAppearance];
}
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
[self synchronizeNSViewAppearance];
}
- (void)setCenter:(CGPoint)center {
[super setCenter:center];
[self synchronizeNSViewAppearance];
}
- (void)didAddSubview:(TUIView *)subview {
NSAssert(NO, @"%@ must be a leaf in the TwUI hierarchy, should not have added subview: %@", self, subview);
[super didAddSubview:subview];
}
- (BOOL)isRenderingContainedView {
return _renderingContainedViewCount > 0;
}
#pragma mark Lifecycle
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (!self)
return nil;
self.layer.masksToBounds = NO;
self.clearsContextBeforeDrawing = NO;
self.opaque = NO;
// prevents the layer from displaying until we need to render our contained
// view
self.contentMode = TUIViewContentModeScaleToFill;
return self;
}
- (id)initWithNSView:(NSView *)view; {
NSAssert1([NSThread isMainThread], @"%s should only be called from the main thread", __func__);
self = [self initWithFrame:view.frame];
if (!self)
return nil;
self.rootView = view;
return self;
}
- (void)dealloc {
self.rootView.hostView = nil;
}
#pragma mark Geometry
- (void)synchronizeNSViewAppearance; {
NSAssert1([NSThread isMainThread], @"%s should only be called from the main thread", __func__);
// update the view's hiddenness based on the TwUI hierarchy
BOOL shouldBeHidden = NO;
TUIView *view = self;
while (view != nil) {
if (view.hidden) {
shouldBeHidden = YES;
break;
}
view = view.superview;
}
self.rootView.hidden = shouldBeHidden;
if (!self.nsWindow) {
// can't do this without being in a window
return;
}
NSAssert(self.ancestorTUINSView, @"%@ should be in an TUINSView if it has a window", self);
CGRect frame = self.NSViewFrame;
self.rootView.frame = frame;
[self.ancestorTUINSView recalculateNSViewClipping];
}
#pragma mark Drawing
- (void)drawRect:(CGRect)rect {
if (!self.renderingContainedView) {
return;
}
CGContextRef context = [NSGraphicsContext currentContext].graphicsPort;
CGContextSaveGState(context);
CGContextClearRect(context, self.bounds);
SInt32 major, minor;
Gestalt(gestaltSystemVersionMajor, &major);
Gestalt(gestaltSystemVersionMinor, &minor);
// 10.8 seems to have changed whether -renderInContext: renders the NSView
// flipped or not.
BOOL needsToFlip = (major > 10 || (major == 10 && minor >= 8)) ? [self.rootView isFlipped] : ![self.rootView isFlipped];
if (needsToFlip) {
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1, -1);
}
[self.rootView.layer renderInContext:context];
CGContextRestoreGState(context);
}
- (void)startRenderingContainedView; {
if (_renderingContainedViewCount++ == 0) {
[CATransaction tui_performWithDisabledActions:^{
[self synchronizeNSViewAppearance];
[self.rootView displayIfNeeded];
[self.layer display];
}];
}
}
- (void)stopRenderingContainedView; {
NSAssert(_renderingContainedViewCount > 0, @"Mismatched call to %s", __func__);
if (--_renderingContainedViewCount == 0) {
self.layer.contents = nil;
}
}
#pragma mark View hierarchy
- (void)ancestorDidLayout; {
[self synchronizeNSViewAppearance];
[super ancestorDidLayout];
}
- (void)willMoveToTUINSView:(TUINSView *)view; {
[super willMoveToTUINSView:view];
[self.rootView willMoveToTUINSView:view];
[CATransaction tui_performWithDisabledActions:^{
[self.rootView removeFromSuperview];
}];
}
- (void)didMoveFromTUINSView:(TUINSView *)view; {
[super didMoveFromTUINSView:view];
TUINSView *newView = self.ancestorTUINSView;
if (newView) {
[CATransaction tui_performWithDisabledActions:^{
[newView.appKitHostView addSubview:self.rootView];
}];
self.rootView.nextResponder = self;
}
[self.rootView didMoveFromTUINSView:view];
}
- (void)viewHierarchyDidChange {
[super viewHierarchyDidChange];
// verify that TUIViewNSViewContainers are on top of other subviews
#if DEBUG
NSArray *siblings = self.superview.subviews;
__block BOOL foundTUIView = NO;
[siblings enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(TUIView *view, NSUInteger index, BOOL *stop){
if ([view isKindOfClass:[TUIViewNSViewContainer class]]) {
NSAssert2(!foundTUIView, @"%@ must be on top of its sibling TUIViews: %@", view, siblings);
} else {
foundTUIView = YES;
}
}];
#endif
[self.ancestorTUINSView recalculateNSViewOrdering];
[self synchronizeNSViewAppearance];
[self.rootView viewHierarchyDidChange];
}
- (id<TUIBridgedView>)descendantViewAtPoint:(CGPoint)point {
if (![self pointInside:point])
return nil;
CGPoint NSViewPoint = [self.rootView convertFromWindowPoint:[self convertToWindowPoint:point]];
// never return 'self', since we don't want to catch clicks that didn't
// directly hit the NSView
return [self.rootView descendantViewAtPoint:NSViewPoint];
}
#pragma mark Layout
- (void)layoutSubviews {
[super layoutSubviews];
[self synchronizeNSViewAppearance];
}
- (CGSize)sizeThatFits:(CGSize)constraint {
NSAssert1([NSThread isMainThread], @"%s should only be called from the main thread", __func__);
id view = self.rootView;
NSSize cellSize = NSMakeSize(10000, 10000);
NSCell *cell = nil;
if ([view respondsToSelector:@selector(cell)]) {
cell = [view cell];
}
if ([cell respondsToSelector:@selector(cellSize)]) {
cellSize = [cell cellSize];
}
// if we don't have a cell, or it didn't give us a true size
if (CGSizeEqualToSize(cellSize, CGSizeMake(10000, 10000))) {
return [super sizeThatFits:constraint];
}
return cellSize;
}
- (void)sizeToFit {
if ([self.rootView respondsToSelector:@selector(sizeToFit)]) {
[self.rootView performSelector:@selector(sizeToFit)];
self.bounds = self.rootView.bounds;
} else {
[super sizeToFit];
}
}
#pragma mark NSObject overrides
- (NSString *)description {
return [NSString stringWithFormat:@"<%@ %p> frame = %@, NSView = %@ %@", [self class], self, NSStringFromRect(self.frame), self.rootView, NSStringFromRect(self.rootView.frame)];
}
@end