From c85ebc5b414d14e0a4069712e7876356140a8921 Mon Sep 17 00:00:00 2001 From: joshaber Date: Fri, 13 Apr 2012 11:51:37 -0400 Subject: [PATCH] added TUIPopover --- TwUI.xcodeproj/project.pbxproj | 32 ++ lib/Support/CAAnimation+TUIExtensions.h | 29 ++ lib/Support/CAAnimation+TUIExtensions.m | 42 ++ lib/UIKit/TUIKit.h | 2 + lib/UIKit/TUIPopover.h | 74 +++ lib/UIKit/TUIPopover.m | 599 ++++++++++++++++++++++++ 6 files changed, 778 insertions(+) create mode 100644 lib/Support/CAAnimation+TUIExtensions.h create mode 100644 lib/Support/CAAnimation+TUIExtensions.m create mode 100644 lib/UIKit/TUIPopover.h create mode 100644 lib/UIKit/TUIPopover.m diff --git a/TwUI.xcodeproj/project.pbxproj b/TwUI.xcodeproj/project.pbxproj index 01390790..967dfa22 100644 --- a/TwUI.xcodeproj/project.pbxproj +++ b/TwUI.xcodeproj/project.pbxproj @@ -71,6 +71,18 @@ 8819794F13E26E5800AA39EB /* TUINSView+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */; }; 8819795013E26E5800AA39EB /* TUINSView+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */; }; 8819795113E26E5800AA39EB /* TUINSView+Accessibility.m in Sources */ = {isa = PBXBuildFile; fileRef = 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */; }; + 884E8F5215387E11000F7A8D /* TUIPopover.h in Headers */ = {isa = PBXBuildFile; fileRef = 884E8F5015387E11000F7A8D /* TUIPopover.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 884E8F5315387E11000F7A8D /* TUIPopover.h in Headers */ = {isa = PBXBuildFile; fileRef = 884E8F5015387E11000F7A8D /* TUIPopover.h */; }; + 884E8F5415387E11000F7A8D /* TUIPopover.h in Headers */ = {isa = PBXBuildFile; fileRef = 884E8F5015387E11000F7A8D /* TUIPopover.h */; }; + 884E8F5515387E11000F7A8D /* TUIPopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 884E8F5115387E11000F7A8D /* TUIPopover.m */; }; + 884E8F5615387E11000F7A8D /* TUIPopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 884E8F5115387E11000F7A8D /* TUIPopover.m */; }; + 884E8F5715387E11000F7A8D /* TUIPopover.m in Sources */ = {isa = PBXBuildFile; fileRef = 884E8F5115387E11000F7A8D /* TUIPopover.m */; }; + 884E8F5B1538809C000F7A8D /* CAAnimation+TUIExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 884E8F591538809C000F7A8D /* CAAnimation+TUIExtensions.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 884E8F5C1538809C000F7A8D /* CAAnimation+TUIExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 884E8F591538809C000F7A8D /* CAAnimation+TUIExtensions.h */; }; + 884E8F5D1538809C000F7A8D /* CAAnimation+TUIExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 884E8F591538809C000F7A8D /* CAAnimation+TUIExtensions.h */; }; + 884E8F5E1538809C000F7A8D /* CAAnimation+TUIExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 884E8F5A1538809C000F7A8D /* CAAnimation+TUIExtensions.m */; }; + 884E8F5F1538809C000F7A8D /* CAAnimation+TUIExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 884E8F5A1538809C000F7A8D /* CAAnimation+TUIExtensions.m */; }; + 884E8F601538809C000F7A8D /* CAAnimation+TUIExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 884E8F5A1538809C000F7A8D /* CAAnimation+TUIExtensions.m */; }; 886EBA7F13D64393006DE018 /* TUIControl+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 886EBA7D13D64393006DE018 /* TUIControl+Private.h */; }; 886EBA8013D64393006DE018 /* TUIControl+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 886EBA7D13D64393006DE018 /* TUIControl+Private.h */; }; 886EBA8113D64393006DE018 /* TUIControl+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 886EBA7D13D64393006DE018 /* TUIControl+Private.h */; }; @@ -272,6 +284,10 @@ 8819794313E26E0200AA39EB /* TUIView+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIView+Accessibility.m"; sourceTree = ""; }; 8819794A13E26E5800AA39EB /* TUINSView+Accessibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUINSView+Accessibility.h"; sourceTree = ""; }; 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUINSView+Accessibility.m"; sourceTree = ""; }; + 884E8F5015387E11000F7A8D /* TUIPopover.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUIPopover.h; sourceTree = ""; }; + 884E8F5115387E11000F7A8D /* TUIPopover.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TUIPopover.m; sourceTree = ""; }; + 884E8F591538809C000F7A8D /* CAAnimation+TUIExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CAAnimation+TUIExtensions.h"; sourceTree = ""; }; + 884E8F5A1538809C000F7A8D /* CAAnimation+TUIExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "CAAnimation+TUIExtensions.m"; sourceTree = ""; }; 886EBA7D13D64393006DE018 /* TUIControl+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUIControl+Private.h"; sourceTree = ""; }; 886EBA7E13D64393006DE018 /* TUIControl+Private.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUIControl+Private.m"; sourceTree = ""; }; 887F272A13F9969800D75DE6 /* TUITableViewSectionHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUITableViewSectionHeader.h; sourceTree = ""; }; @@ -533,6 +549,8 @@ CBB74C3A13BE6E1900C85CB5 /* ABActiveRange.m */, CBB74C3B13BE6E1900C85CB5 /* CoreText+Additions.h */, CBB74C3C13BE6E1900C85CB5 /* CoreText+Additions.m */, + 884E8F591538809C000F7A8D /* CAAnimation+TUIExtensions.h */, + 884E8F5A1538809C000F7A8D /* CAAnimation+TUIExtensions.m */, ); name = Support; path = lib/Support; @@ -587,6 +605,8 @@ 8819794B13E26E5800AA39EB /* TUINSView+Accessibility.m */, CBB74C6313BE6E1900C85CB5 /* TUINSWindow.h */, CBB74C6413BE6E1900C85CB5 /* TUINSWindow.m */, + 884E8F5015387E11000F7A8D /* TUIPopover.h */, + 884E8F5115387E11000F7A8D /* TUIPopover.m */, CBB74C6513BE6E1900C85CB5 /* TUIResponder.h */, CBB74C6613BE6E1900C85CB5 /* TUIResponder.m */, CBB74C6713BE6E1900C85CB5 /* TUIScrollKnob.h */, @@ -661,6 +681,8 @@ 88EFFB5313F417E200CF91A9 /* TUITextViewEditor.h in Headers */, 88D25F5713F5D96500CFAAA9 /* TUITableView+Cell.h in Headers */, 887F272E13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, + 884E8F5415387E11000F7A8D /* TUIPopover.h in Headers */, + 884E8F5D1538809C000F7A8D /* CAAnimation+TUIExtensions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -702,6 +724,7 @@ CBB74CD313BE6E1900C85CB5 /* TUITextRenderer.h in Headers */, CBB74CD513BE6E1900C85CB5 /* TUITextView.h in Headers */, CBB74CD713BE6E1900C85CB5 /* TUITooltipWindow.h in Headers */, + 884E8F5B1538809C000F7A8D /* CAAnimation+TUIExtensions.h in Headers */, CBB74CDA13BE6E1900C85CB5 /* TUIView+Event.h in Headers */, CBB74CDC13BE6E1900C85CB5 /* TUIView+NSTextInputClient.h in Headers */, CBB74CDE13BE6E1900C85CB5 /* TUIView+PasteboardDragging.h in Headers */, @@ -710,6 +733,7 @@ CBB74CE413BE6E1900C85CB5 /* TUIViewController.h in Headers */, CBB74CE613BE6E1900C85CB5 /* TUIViewNSViewContainer.h in Headers */, 887F272C13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, + 884E8F5215387E11000F7A8D /* TUIPopover.h in Headers */, 886EBA7F13D64393006DE018 /* TUIControl+Private.h in Headers */, 8819794413E26E0200AA39EB /* TUIView+Accessibility.h in Headers */, 8819794C13E26E5800AA39EB /* TUINSView+Accessibility.h in Headers */, @@ -733,6 +757,8 @@ 88EFFB5213F417E200CF91A9 /* TUITextViewEditor.h in Headers */, 88D25F5613F5D96500CFAAA9 /* TUITableView+Cell.h in Headers */, 887F272D13F9969800D75DE6 /* TUITableViewSectionHeader.h in Headers */, + 884E8F5315387E11000F7A8D /* TUIPopover.h in Headers */, + 884E8F5C1538809C000F7A8D /* CAAnimation+TUIExtensions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -933,6 +959,8 @@ 88EFFB5613F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5A13F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, 887F273113F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, + 884E8F5715387E11000F7A8D /* TUIPopover.m in Sources */, + 884E8F601538809C000F7A8D /* CAAnimation+TUIExtensions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -995,6 +1023,8 @@ 88D25F5813F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, 887F272F13F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, 88A4AFDF145A16CA0071CF22 /* TUITextRenderer+Accessibility.m in Sources */, + 884E8F5515387E11000F7A8D /* TUIPopover.m in Sources */, + 884E8F5E1538809C000F7A8D /* CAAnimation+TUIExtensions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1065,6 +1095,8 @@ 88EFFB5513F417E200CF91A9 /* TUITextViewEditor.m in Sources */, 88D25F5913F5D96500CFAAA9 /* TUITableView+Cell.m in Sources */, 887F273013F9969800D75DE6 /* TUITableViewSectionHeader.m in Sources */, + 884E8F5615387E11000F7A8D /* TUIPopover.m in Sources */, + 884E8F5F1538809C000F7A8D /* CAAnimation+TUIExtensions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/lib/Support/CAAnimation+TUIExtensions.h b/lib/Support/CAAnimation+TUIExtensions.h new file mode 100644 index 00000000..af4e0393 --- /dev/null +++ b/lib/Support/CAAnimation+TUIExtensions.h @@ -0,0 +1,29 @@ +/* + Copyright 2012 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. + */ + +#import + +#import + +typedef void (^TUICAAnimationCompletionBlock)(); + +//Note this is slightly flawed as we set ourself as the delegate, really we should create a chained proxy, if we need that I will add it. + +@interface CAAnimation (TUIExtensions) + +@property (nonatomic, copy) TUICAAnimationCompletionBlock tui_completionBlock; + +@end diff --git a/lib/Support/CAAnimation+TUIExtensions.m b/lib/Support/CAAnimation+TUIExtensions.m new file mode 100644 index 00000000..c075f7ed --- /dev/null +++ b/lib/Support/CAAnimation+TUIExtensions.m @@ -0,0 +1,42 @@ +/* + Copyright 2012 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. + */ + +#import "CAAnimation+TUIExtensions.h" + +#import + +NSString *TUICAAnimationCompletionBlockAssociatedObjectKey = @"TUICAAnimationCompletionBlockAssociatedObjectKey"; + +@implementation CAAnimation (TUIExtensions) + +- (void)setTui_completionBlock:(TUICAAnimationCompletionBlock)block +{ + self.delegate = self; + objc_setAssociatedObject(self, &TUICAAnimationCompletionBlockAssociatedObjectKey, block, OBJC_ASSOCIATION_COPY); +} + +- (TUICAAnimationCompletionBlock)tui_completionBlock +{ + return objc_getAssociatedObject(self, &TUICAAnimationCompletionBlockAssociatedObjectKey); +} + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag +{ + if (flag && self.tui_completionBlock != nil) + self.tui_completionBlock(); +} + +@end diff --git a/lib/UIKit/TUIKit.h b/lib/UIKit/TUIKit.h index a93d2cc1..49c1b00f 100644 --- a/lib/UIKit/TUIKit.h +++ b/lib/UIKit/TUIKit.h @@ -41,6 +41,8 @@ #import "TUICGAdditions.h" #import "CoreText+Additions.h" #import "TUITextEditor.h" +#import "TUIPopover.h" +#import "CAAnimation+TUIExtensions.h" extern CGContextRef TUIGraphicsGetCurrentContext(void); extern void TUIGraphicsPushContext(CGContextRef context); diff --git a/lib/UIKit/TUIPopover.h b/lib/UIKit/TUIPopover.h new file mode 100644 index 00000000..011fdfe3 --- /dev/null +++ b/lib/UIKit/TUIPopover.h @@ -0,0 +1,74 @@ +/* + Copyright 2012 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. + */ + +#import "TUIView.h" + +@class TUIPopover; +@class TUIViewController; +@class TUIColor; + +enum _TUIPopoverViewControllerBehaviour +{ + TUIPopoverViewControllerBehaviourApplicationDefined = 0, + TUIPopoverViewControllerBehaviourTransient = 1, + TUIPopoverViewControllerBehaviourSemiTransient = 2 //Currently not supported, here for forwards compatibility purposes +}; + +typedef NSUInteger TUIPopoverViewControllerBehaviour; + +typedef void (^TUIPopoverDelegateBlock)(TUIPopover *popover); + +@interface TUIPopover : NSResponder + +@property (nonatomic, strong) TUIViewController *contentViewController; +@property (nonatomic, unsafe_unretained) Class backgroundViewClass; //Must be a subclass of TUIPopoverBackgroundView +@property (nonatomic, unsafe_unretained) CGSize contentSize; //CGSizeZero uses the size of the view on contentViewController +@property (nonatomic, unsafe_unretained) BOOL animates; +@property (nonatomic, unsafe_unretained) TUIPopoverViewControllerBehaviour behaviour; +@property (nonatomic, readonly) BOOL shown; +@property (nonatomic, readonly) CGRect positioningRect; + +//Block callbacks +@property (nonatomic, copy) TUIPopoverDelegateBlock willCloseBlock; +@property (nonatomic, copy) TUIPopoverDelegateBlock didCloseBlock; + +@property (nonatomic, copy) TUIPopoverDelegateBlock willShowBlock; +@property (nonatomic, copy) TUIPopoverDelegateBlock didShowBlock; + +- (id)initWithContentViewController:(TUIViewController *)viewController; + +- (void)showRelativeToRect:(CGRect)positioningRect ofView:(TUIView *)positioningView preferredEdge:(CGRectEdge)preferredEdge; + +- (void)close; +- (void)closeWithFadeoutDuration:(NSTimeInterval)duration; +- (IBAction)performClose:(id)sender; + +@end + +@interface TUIPopoverBackgroundView : TUIView + ++ (CGSize)sizeForBackgroundViewWithContentSize:(CGSize)contentSize popoverEdge:(CGRectEdge)popoverEdge; ++ (CGRect)contentViewFrameForBackgroundFrame:(CGRect)frame popoverEdge:(CGRectEdge)popoverEdge; ++ (TUIPopoverBackgroundView *)backgroundViewForContentSize:(CGSize)contentSize popoverEdge:(CGRectEdge)popoverEdge originScreenRect:(CGRect)originScreenRect; + +- (id)initWithFrame:(CGRect)frame popoverEdge:(CGRectEdge)popoverEdge originScreenRect:(CGRect)originScreenRect; +- (CGPathRef)newPopoverPathForEdge:(CGRectEdge)popoverEdge inFrame:(CGRect)frame; //override in subclasses to change the shape of the popover, but still use the default drawing. + +//Used in the default implementation +@property (nonatomic, strong) TUIColor *strokeColor; +@property (nonatomic, strong) TUIColor *fillColor; + +@end diff --git a/lib/UIKit/TUIPopover.m b/lib/UIKit/TUIPopover.m new file mode 100644 index 00000000..83753d89 --- /dev/null +++ b/lib/UIKit/TUIPopover.m @@ -0,0 +1,599 @@ +/* + Copyright 2012 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. + */ + +#import "TUIPopover.h" +#import "TUINSWindow.h" +#import "TUIViewController.h" + +#import "CAAnimation+TUIExtensions.h" + +//#import "GHUICoreGraphics.h" + +//*************************************************************************** + +@interface TUIPopoverBackgroundView () + +@property (nonatomic, unsafe_unretained) CGRect screenOriginRect; +@property (nonatomic, unsafe_unretained) CGRectEdge popoverEdge; + +- (CGRectEdge)arrowEdgeForPopoverEdge:(CGRectEdge)popoverEdge; +- (void)updateMaskLayer; + +@end + +//*************************************************************************** + +@interface TUIPopoverWindowContentView : NSView + +@property (nonatomic, readonly) TUINSView *nsView; +@property (nonatomic, unsafe_unretained) CGRectEdge arrowEdge; + +@end + +//*************************************************************************** + +NSTimeInterval const TUIPopoverDefaultFadeoutDuration = 0.3; + +//*************************************************************************** + +@interface TUIPopover () + +@property (nonatomic, strong) TUINSWindow *popoverWindow; +@property (nonatomic, unsafe_unretained) id transientEventMonitor; +@property (nonatomic, unsafe_unretained) BOOL animating; +@property (nonatomic, assign) CGSize originalViewSize; + +- (void)removeEventMonitor; + +@end + +//*************************************************************************** + +@implementation TUIPopover + +@synthesize contentViewController = _contentViewController; +@synthesize backgroundViewClass = _backgroundViewClass; +@synthesize contentSize = _contentSize; +@synthesize animates = _animates; +@synthesize behaviour = _behaviour; +@synthesize positioningRect = _positioningRect; +@synthesize willCloseBlock = _willCloseBlock; +@synthesize didCloseBlock = _didCloseBlock; +@synthesize willShowBlock = _willShowBlock; +@synthesize didShowBlock = _didShowBlock; + +@synthesize popoverWindow = _popoverWindow; +@synthesize transientEventMonitor = _transientEventMonitor; +@synthesize animating = _animating; + +@synthesize originalViewSize = _originalViewSize; + +- (id)initWithContentViewController:(TUIViewController *)viewController +{ + self = [super init]; + if (self == nil) + return nil; + + _contentViewController = viewController; + _backgroundViewClass = [TUIPopoverBackgroundView class]; + _behaviour = TUIPopoverViewControllerBehaviourApplicationDefined; + + return self; +} + +#pragma mark - +#pragma mark Derived Properties + +- (BOOL)shown +{ + return (self.popoverWindow.contentView != nil); +} + +#pragma mark - +#pragma mark Showing + +- (void)showRelativeToRect:(CGRect)positioningRect ofView:(TUIView *)positioningView preferredEdge:(CGRectEdge)preferredEdge +{ + if (self.shown) + return; + + [self.contentViewController viewWillAppear:YES]; //this will always be animated… in the current implementation + + if (self.willShowBlock != nil) + self.willShowBlock(self); + + if (self.behaviour != TUIPopoverViewControllerBehaviourApplicationDefined) { + if (self.transientEventMonitor != nil) { + [self removeEventMonitor]; + } + + self.transientEventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:(NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyUpMask) handler: ^ (NSEvent *event) { + if (self.popoverWindow == nil) + return event; + + static NSUInteger escapeKey = 53; + BOOL shouldClose = (event.type == NSLeftMouseDown || event.type == NSRightMouseDown ? (!NSPointInRect([NSEvent mouseLocation], self.popoverWindow.frame) && self.behaviour == TUIPopoverViewControllerBehaviourTransient) : event.keyCode == escapeKey); + + if (shouldClose) { + [self close]; + } + + return event; + }]; + } + + if (CGRectEqualToRect(positioningRect, CGRectZero)) + positioningRect = [positioningView bounds]; + + CGRect basePositioningRect = [positioningView convertRect:positioningRect toView:nil]; + NSRect windowRelativeRect = [positioningView.nsView convertRect:basePositioningRect toView:nil]; + CGRect screenPositioningRect = windowRelativeRect; + screenPositioningRect.origin = [positioningView.nsWindow convertBaseToScreen:windowRelativeRect.origin]; + self.originalViewSize = self.contentViewController.view.frame.size; + CGSize contentViewSize = (CGSizeEqualToSize(self.contentSize, CGSizeZero) ? self.contentViewController.view.frame.size : self.contentSize); + + CGRect (^popoverRectForEdge)(CGRectEdge) = ^ (CGRectEdge popoverEdge) + { + CGSize popoverSize = [self.backgroundViewClass sizeForBackgroundViewWithContentSize:contentViewSize popoverEdge:popoverEdge]; + CGRect returnRect = NSMakeRect(0.0, 0.0, popoverSize.width, popoverSize.height); + if (popoverEdge == CGRectMinYEdge) { + CGFloat xOrigin = NSMidX(screenPositioningRect) - floor(popoverSize.width / 2.0); + CGFloat yOrigin = NSMinY(screenPositioningRect) - popoverSize.height; + returnRect.origin = NSMakePoint(xOrigin, yOrigin); + } else if (popoverEdge == CGRectMaxYEdge) { + CGFloat xOrigin = NSMidX(screenPositioningRect) - floor(popoverSize.width / 2.0); + returnRect.origin = NSMakePoint(xOrigin, NSMaxY(screenPositioningRect)); + } else if (popoverEdge == CGRectMinXEdge) { + CGFloat xOrigin = NSMinX(screenPositioningRect) - popoverSize.width; + CGFloat yOrigin = NSMidY(screenPositioningRect) - floor(popoverSize.height / 2.0); + returnRect.origin = NSMakePoint(xOrigin, yOrigin); + } else if (popoverEdge == CGRectMaxXEdge) { + CGFloat yOrigin = NSMidY(screenPositioningRect) - floor(popoverSize.height / 2.0); + returnRect.origin = NSMakePoint(NSMaxX(screenPositioningRect), yOrigin); + } else { + returnRect = CGRectZero; + } + + return returnRect; + }; + + BOOL (^checkPopoverSizeForScreenWithPopoverEdge)(CGRectEdge) = ^ (CGRectEdge popoverEdge) + { + CGRect popoverRect = popoverRectForEdge(popoverEdge); + return NSContainsRect(positioningView.nsWindow.screen.frame, popoverRect); + }; + + //This is as ugly as sin… but it gets the job done. I couldn't think of a nice way to code this but still get the desired behaviour + __block CGRectEdge popoverEdge = preferredEdge; + CGRect (^popoverRect)() = ^ + { + CGRectEdge (^nextEdgeForEdge)(CGRectEdge) = ^ (CGRectEdge currentEdge) + { + if (currentEdge == CGRectMaxXEdge) { + return (CGRectEdge)(preferredEdge == CGRectMinXEdge ? CGRectMaxYEdge : CGRectMinXEdge); + } else if (currentEdge == CGRectMinXEdge) { + return (CGRectEdge)(preferredEdge == CGRectMaxXEdge ? CGRectMaxYEdge : CGRectMaxXEdge); + } else if (currentEdge == CGRectMaxYEdge) { + return (CGRectEdge)(preferredEdge == CGRectMinYEdge ? CGRectMaxXEdge : CGRectMinYEdge); + } else if (currentEdge == CGRectMinYEdge) { + return (CGRectEdge)(preferredEdge == CGRectMaxYEdge ? CGRectMaxXEdge : CGRectMaxYEdge); + } + + return currentEdge; + }; + + CGRect (^fitRectToScreen)(CGRect) = ^ (CGRect proposedRect) { + NSRect screenRect = positioningView.nsWindow.screen.frame; + + if (proposedRect.origin.y < NSMinY(screenRect)) + proposedRect.origin.y = NSMinY(screenRect); + if (proposedRect.origin.x < NSMinX(screenRect)) + proposedRect.origin.x = NSMinX(screenRect); + + if (NSMaxY(proposedRect) > NSMaxY(screenRect)) + proposedRect.origin.y = (NSMaxY(screenRect) - NSHeight(proposedRect)); + if (NSMaxX(proposedRect) > NSMaxX(screenRect)) + proposedRect.origin.x = (NSMaxX(screenRect) - NSWidth(proposedRect)); + + return proposedRect; + }; + + NSUInteger attemptCount = 0; + while (!checkPopoverSizeForScreenWithPopoverEdge(popoverEdge)) { + if (attemptCount > 4) { + popoverEdge = preferredEdge; + return fitRectToScreen(popoverRectForEdge(popoverEdge)); + break; + } + + popoverEdge = nextEdgeForEdge(popoverEdge); + attemptCount ++; + } + + return (CGRect)popoverRectForEdge(popoverEdge); + }; + + CGRect popoverScreenRect = popoverRect(); + TUIPopoverBackgroundView *backgroundView = [self.backgroundViewClass backgroundViewForContentSize:contentViewSize popoverEdge:popoverEdge originScreenRect:screenPositioningRect]; + + CGRect contentViewFrame = [self.backgroundViewClass contentViewFrameForBackgroundFrame:backgroundView.bounds popoverEdge:popoverEdge]; + self.contentViewController.view.frame = contentViewFrame; + [backgroundView addSubview:self.contentViewController.view]; + self.popoverWindow = [[TUINSWindow alloc] initWithContentRect:popoverScreenRect]; + [self.popoverWindow setReleasedWhenClosed:NO]; + TUIPopoverWindowContentView *contentView = [[TUIPopoverWindowContentView alloc] initWithFrame:backgroundView.bounds]; + contentView.arrowEdge = [backgroundView arrowEdgeForPopoverEdge:popoverEdge]; + contentView.nsView.rootView = backgroundView; + [self.popoverWindow setOpaque:NO]; + [self.popoverWindow setBackgroundColor:[NSColor clearColor]]; + self.popoverWindow.contentView = contentView; + self.popoverWindow.alphaValue = 0.0; + [positioningView.nsWindow addChildWindow:self.popoverWindow ordered:NSWindowAbove]; + [self.popoverWindow makeKeyAndOrderFront:self]; + [backgroundView updateMaskLayer]; + + CABasicAnimation *fadeInAnimation = [CABasicAnimation animationWithKeyPath:@"alphaValue"]; + fadeInAnimation.duration = 0.3; + fadeInAnimation.tui_completionBlock = ^ { + self.animating = NO; + [self.contentViewController viewDidAppear:YES]; + + if (self.didShowBlock) + self.didShowBlock(self); + }; + + self.popoverWindow.animations = [NSDictionary dictionaryWithObject:fadeInAnimation forKey:@"alphaValue"]; + self.animating = YES; + [self.popoverWindow.animator setAlphaValue:1.0]; +} + +#pragma mark - +#pragma mark Closing + +- (void)close +{ + [self closeWithFadeoutDuration:TUIPopoverDefaultFadeoutDuration]; +} + +- (void)closeWithFadeoutDuration:(NSTimeInterval)duration +{ + if (self.animating) + return; + + if (self.transientEventMonitor != nil) { + [self removeEventMonitor]; + } + + if (self.willCloseBlock != nil) + self.willCloseBlock(self); + + CABasicAnimation *fadeOutAnimation = [CABasicAnimation animationWithKeyPath:@"alphaValue"]; + fadeOutAnimation.duration = duration; + fadeOutAnimation.tui_completionBlock = ^ { + [self.popoverWindow.parentWindow removeChildWindow:self.popoverWindow]; + [self.popoverWindow close]; + self.popoverWindow.contentView = nil; + self.animating = NO; + + if (self.didCloseBlock != nil) + self.didCloseBlock(self); + + self.contentViewController.view.frame = CGRectMake(self.contentViewController.view.frame.origin.x, self.contentViewController.view.frame.origin.y, self.originalViewSize.width, self.originalViewSize.height); + }; + + self.popoverWindow.animations = [NSDictionary dictionaryWithObject:fadeOutAnimation forKey:@"alphaValue"]; + self.animating = YES; + [self.popoverWindow.animator setAlphaValue:0.0]; +} + +- (IBAction)performClose:(id)sender +{ + [self close]; +} + +#pragma mark - +#pragma mark Event Monitor + +- (void)removeEventMonitor +{ + [NSEvent removeMonitor:self.transientEventMonitor]; + self.transientEventMonitor = nil; +} + +@end + +//*************************************************************************** + +CGFloat const TUIPopoverBackgroundViewBorderRadius = 5.0; +CGFloat const TUIPopoverBackgroundViewArrowHeight = 17.0; +CGFloat const TUIPopoverBackgroundViewArrowWidth = 35.0; + +//*************************************************************************** + +@implementation TUIPopoverBackgroundView + +@synthesize fillColor = _fillColor; +@synthesize strokeColor = _strokeColor; + +@synthesize screenOriginRect = _screenOriginRect; +@synthesize popoverEdge = _popoverEdge; + ++ (CGSize)sizeForBackgroundViewWithContentSize:(CGSize)contentSize popoverEdge:(CGRectEdge)popoverEdge +{ + CGSize returnSize = contentSize; + if (popoverEdge == CGRectMaxXEdge || popoverEdge == CGRectMinXEdge) { + returnSize.width += TUIPopoverBackgroundViewArrowHeight; + } else { + returnSize.height += TUIPopoverBackgroundViewArrowHeight; + } + + returnSize.width ++; + returnSize.height ++; + + return returnSize; +} + ++ (CGRect)contentViewFrameForBackgroundFrame:(CGRect)backgroundFrame popoverEdge:(CGRectEdge)popoverEdge +{ + CGRect returnFrame = NSInsetRect(backgroundFrame, 1.0, 1.0); + switch (popoverEdge) { + case CGRectMinXEdge: + returnFrame.size.width -= TUIPopoverBackgroundViewArrowHeight; + break; + case CGRectMinYEdge: + returnFrame.size.height -= TUIPopoverBackgroundViewArrowHeight; + break; + case CGRectMaxXEdge: + returnFrame.size.width -= TUIPopoverBackgroundViewArrowHeight; + returnFrame.origin.x += TUIPopoverBackgroundViewArrowHeight; + break; + case CGRectMaxYEdge: + returnFrame.size.height -= TUIPopoverBackgroundViewArrowHeight; + returnFrame.origin.y += TUIPopoverBackgroundViewArrowHeight; + break; + default: + break; + } + + return returnFrame; +} + ++ (TUIPopoverBackgroundView *)backgroundViewForContentSize:(CGSize)contentSize popoverEdge:(CGRectEdge)popoverEdge originScreenRect:(CGRect)originScreenRect +{ + CGSize size = [self sizeForBackgroundViewWithContentSize:contentSize popoverEdge:popoverEdge]; + TUIPopoverBackgroundView *returnView = [[self.class alloc] initWithFrame:NSMakeRect(0.0, 0.0, size.width, size.height) popoverEdge:popoverEdge originScreenRect:originScreenRect]; + return returnView; +} + +- (CGPathRef)newPopoverPathForEdge:(CGRectEdge)popoverEdge inFrame:(CGRect)frame +{ + CGRectEdge arrowEdge = [self arrowEdgeForPopoverEdge:popoverEdge]; + + CGRect contentRect = CGRectIntegral([[self class] contentViewFrameForBackgroundFrame:frame popoverEdge:self.popoverEdge]); + CGFloat minX = NSMinX(contentRect); + CGFloat maxX = NSMaxX(contentRect); + CGFloat minY = NSMinY(contentRect); + CGFloat maxY = NSMaxY(contentRect); + + CGRect windowRect = self.screenOriginRect; + windowRect.origin = [self.nsWindow convertScreenToBase:self.screenOriginRect.origin]; + CGRect originRect = [self convertRect:windowRect fromView:nil]; //hmm as we have no superview at this point is this retarded? + CGFloat midOriginY = floor(NSMidY(originRect)); + CGFloat midOriginX = floor(NSMidX(originRect)); + + CGFloat maxArrowX = 0.0; + CGFloat minArrowX = 0.0; + CGFloat minArrowY = 0.0; + CGFloat maxArrowY = 0.0; + + // Even I have no idea at this point… :trollface: + // So we don't have a weird arrow situation we need to make sure we draw it within the radius. + // If we have to nudge it then we have to shrink the arrow as otherwise it looks all wonky and weird. + // That is what this complete mess below does. + + if (arrowEdge == CGRectMinYEdge || arrowEdge == CGRectMaxYEdge) { + maxArrowX = floor(midOriginX + (TUIPopoverBackgroundViewArrowWidth / 2.0)); + CGFloat maxPossible = (NSMaxX(contentRect) - TUIPopoverBackgroundViewBorderRadius); + if (maxArrowX > maxPossible) { + CGFloat delta = maxArrowX - maxPossible; + maxArrowX = maxPossible; + minArrowX = maxArrowX - (TUIPopoverBackgroundViewArrowWidth - delta); + } else { + minArrowX = floor(midOriginX - (TUIPopoverBackgroundViewArrowWidth / 2.0)); + if (minArrowX < TUIPopoverBackgroundViewBorderRadius) { + CGFloat delta = TUIPopoverBackgroundViewBorderRadius - minArrowX; + minArrowX = TUIPopoverBackgroundViewBorderRadius; + maxArrowX = minArrowX + (TUIPopoverBackgroundViewArrowWidth - (delta * 2)); + } + } + } else { + minArrowY = floor(midOriginY - (TUIPopoverBackgroundViewArrowWidth / 2.0)); + if (minArrowY < TUIPopoverBackgroundViewBorderRadius) { + CGFloat delta = TUIPopoverBackgroundViewBorderRadius - minArrowY; + minArrowY = TUIPopoverBackgroundViewBorderRadius; + maxArrowY = minArrowY + (TUIPopoverBackgroundViewArrowWidth - (delta * 2)); + } else { + maxArrowY = floor(midOriginY + (TUIPopoverBackgroundViewArrowWidth / 2.0)); + CGFloat maxPossible = (NSMaxY(contentRect) - TUIPopoverBackgroundViewBorderRadius); + if (maxArrowY > maxPossible) { + CGFloat delta = maxArrowY - maxPossible; + maxArrowY = maxPossible; + minArrowY = maxArrowY - (TUIPopoverBackgroundViewArrowWidth - delta); + } + } + } + + CGMutablePathRef path = CGPathCreateMutable(); + CGPathMoveToPoint(path, NULL, minX, floor(minY + TUIPopoverBackgroundViewBorderRadius)); + if (arrowEdge == CGRectMinXEdge) { + CGPathAddLineToPoint(path, NULL, minX, minArrowY); + CGPathAddLineToPoint(path, NULL, floor(minX - TUIPopoverBackgroundViewArrowHeight), midOriginY); + CGPathAddLineToPoint(path, NULL, minX, maxArrowY); + } + + CGPathAddArc(path, NULL, floor(minX + TUIPopoverBackgroundViewBorderRadius), floor(minY + contentRect.size.height - TUIPopoverBackgroundViewBorderRadius), TUIPopoverBackgroundViewBorderRadius, M_PI, M_PI / 2, 1); + if (arrowEdge == CGRectMaxYEdge) { + CGPathAddLineToPoint(path, NULL, minArrowX, maxY); + CGPathAddLineToPoint(path, NULL, midOriginX, floor(maxY + TUIPopoverBackgroundViewArrowHeight)); + CGPathAddLineToPoint(path, NULL, maxArrowX, maxY); + } + + CGPathAddArc(path, NULL, floor(minX + contentRect.size.width - TUIPopoverBackgroundViewBorderRadius), floor(minY + contentRect.size.height - TUIPopoverBackgroundViewBorderRadius), TUIPopoverBackgroundViewBorderRadius, M_PI / 2, 0.0, 1); + if (arrowEdge == CGRectMaxXEdge) { + CGPathAddLineToPoint(path, NULL, maxX, maxArrowY); + CGPathAddLineToPoint(path, NULL, floor(maxX + TUIPopoverBackgroundViewArrowHeight), midOriginY); + CGPathAddLineToPoint(path, NULL, maxX, minArrowY); + } + + CGPathAddArc(path, NULL, floor(contentRect.origin.x + contentRect.size.width - TUIPopoverBackgroundViewBorderRadius), floor(minY + TUIPopoverBackgroundViewBorderRadius), TUIPopoverBackgroundViewBorderRadius, 0.0, -M_PI / 2, 1); + if (arrowEdge == CGRectMinYEdge) { + CGPathAddLineToPoint(path, NULL, maxArrowX, minY); + CGPathAddLineToPoint(path, NULL, midOriginX, floor(minY - TUIPopoverBackgroundViewArrowHeight)); + CGPathAddLineToPoint(path, NULL, minArrowX, minY); + } + + CGPathAddArc(path, NULL, floor(minX + TUIPopoverBackgroundViewBorderRadius), floor(minY + TUIPopoverBackgroundViewBorderRadius), TUIPopoverBackgroundViewBorderRadius, -M_PI / 2, M_PI, 1); + + return path; + +} + +- (id)initWithFrame:(CGRect)frame popoverEdge:(CGRectEdge)popoverEdge originScreenRect:(CGRect)originScreenRect //originScreenRect is in the screen coordinate space +{ + self = [super initWithFrame:frame]; + if (self == nil) + return nil; + + _popoverEdge = popoverEdge; + _screenOriginRect = originScreenRect; + _strokeColor = [TUIColor blackColor]; + _fillColor = [TUIColor whiteColor]; + + __block __unsafe_unretained TUIPopoverBackgroundView *weakSelf = self; + self.drawRect = ^ (TUIView *view, CGRect rect) + { + TUIPopoverBackgroundView *strongSelf = weakSelf; + CGContextRef context = TUIGraphicsGetCurrentContext(); + CGPathRef outerBorder = [strongSelf newPopoverPathForEdge:self.popoverEdge inFrame:self.bounds]; + CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor); + CGContextAddPath(context, outerBorder); + CGContextStrokePath(context); + + CGContextSetFillColorWithColor(context, self.fillColor.CGColor); + CGContextAddPath(context, outerBorder); + CGContextFillPath(context); + + CGPathRelease(outerBorder); + }; + + return self; +} + +- (void)updateMaskLayer +{ + CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init]; + CGPathRef path = [self newPopoverPathForEdge:self.popoverEdge inFrame:self.bounds]; + maskLayer.path = path; + maskLayer.fillColor = CGColorGetConstantColor(kCGColorBlack); + + CGPathRelease(path); + + self.layer.mask = maskLayer; + +} + +- (CGRectEdge)arrowEdgeForPopoverEdge:(CGRectEdge)popoverEdge +{ + CGRectEdge arrowEdge = CGRectMinYEdge; + switch (popoverEdge) { + case CGRectMaxXEdge: + arrowEdge = CGRectMinXEdge; + break; + case CGRectMaxYEdge: + arrowEdge = CGRectMinYEdge; + break; + case CGRectMinXEdge: + arrowEdge = CGRectMaxXEdge; + break; + case CGRectMinYEdge: + arrowEdge = CGRectMaxYEdge; + break; + default: + break; + } + + return arrowEdge; +} + +@end + +// Hmm I'm not sure I like how this takes some of the drawing responsibility away from the background view breaking the extensibility. +// But it works. + +@implementation TUIPopoverWindowContentView + +@synthesize nsView = _nsView; +@synthesize arrowEdge = _arrowEdge;; + +- (id)initWithFrame:(NSRect)frameRect { + self = [super initWithFrame:frameRect]; + if(self == nil) return nil; + + _arrowEdge = CGRectMinYEdge; + _nsView = [[TUINSView alloc] initWithFrame:self.bounds]; + [self.nsView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [self.nsView tui_setOpaque:NO]; + [self addSubview:self.nsView]; + + return self; +} + +- (BOOL)isOpaque { + return NO; +} + +- (void)drawRect:(NSRect)dirtyRect { + [NSGraphicsContext saveGraphicsState]; + + + CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort]; + [[NSColor whiteColor] set]; + + CGRect targetRect = CGRectZero; + switch (self.arrowEdge) { + case CGRectMinYEdge: + targetRect = CGRectMake(1.0f, 1.0f + TUIPopoverBackgroundViewArrowHeight, CGRectGetWidth(self.bounds) - 2.0f, CGRectGetHeight(self.bounds) - TUIPopoverBackgroundViewArrowHeight - 2.0f); + break; + case CGRectMaxXEdge: + targetRect = CGRectMake(1.0f, 1.0f, CGRectGetWidth(self.bounds) - 2.0f - TUIPopoverBackgroundViewArrowHeight, CGRectGetHeight(self.bounds) - 2.0f); + break; + case CGRectMaxYEdge: + targetRect = CGRectMake(1.0f, 1.0f, CGRectGetWidth(self.bounds) - 2.0f, CGRectGetHeight(self.bounds) - 2.0f - TUIPopoverBackgroundViewArrowHeight); + break; + case CGRectMinXEdge: + targetRect = CGRectMake(TUIPopoverBackgroundViewArrowHeight + 1.0f, 1.0f, CGRectGetWidth(self.bounds) - 2.0f - TUIPopoverBackgroundViewArrowHeight, CGRectGetHeight(self.bounds) - 2.0f); + break; + + default: + break; + } + + CGContextFillRoundRect(context, targetRect, TUIPopoverBackgroundViewBorderRadius); + + [NSGraphicsContext restoreGraphicsState]; +} + +@end