├── Demos └── RBLPopoverDemo │ ├── RBLPopoverDemo │ ├── en.lproj │ │ └── InfoPlist.strings │ ├── RBLPopoverDemo-Prefix.pch │ ├── main.m │ ├── RPDContentViewController.h │ ├── RPDAppDelegate.h │ ├── RPDContentViewController.m │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── RBLPopoverDemo-Info.plist │ ├── RPDAppDelegate.m │ └── Base.lproj │ │ └── RPDContentViewController.xib │ └── RBLPopoverDemo.xcodeproj │ └── project.pbxproj ├── Cartfile.private ├── Cartfile.resolved ├── RebelTests ├── _testimage.jpg ├── _testimage.png ├── _testimage@2x.png ├── SwiftSpec.swift ├── RBLScrollViewSpec.m ├── RebelTests-Info.plist ├── NSView+RBLAlignmentAdditionsSpec.m ├── RBLHTMLViewSpec.m ├── NSAttributedString+RBLHTMLAdditionsSpec.m ├── NSFont+RBLFallbackAdditionsSpec.m ├── CAAnimation+RBLBlockAdditionsSpec.m ├── NSView+RBLAnimationAdditionsSpec.m ├── NSColor+RBLCGColorAdditionsSpec.m └── RBLResizableImageSpec.m ├── script ├── schemes.awk ├── targets.awk ├── xctool.awk ├── xcodebuild.awk ├── LICENSE.md ├── bootstrap ├── README.md └── cibuild ├── .gitignore ├── .travis.yml ├── .gitmodules ├── Rebel ├── RBLTableView.m ├── NSObject+RBObjectSizzlingAdditions.h ├── RBLOutlineView.m ├── RBLScrollView.h ├── NSAttributedString+RBLHTMLAdditions.h ├── RBLTableCellView.m ├── RBLHTMLView.h ├── NSTextView+RBLAntialiasingAdditions.h ├── NSImage+RBLResizableImageAdditions.h ├── NSView+RBLAlignmentAdditions.h ├── RBLScrolling.h ├── NSApplication+RBLBlockAdditions.h ├── NSImage+RBLResizableImageAdditions.m ├── RBLTableView.h ├── RBLResizableImage.h ├── CAAnimation+RBLBlockAdditions.h ├── RBLTableCellView.h ├── RBLOutlineView.h ├── NSColor+RBLCGColorAdditions.h ├── NSAttributedString+RBLHTMLAdditions.m ├── RBLClipView.h ├── NSFont+RBLFallbackAdditions.h ├── RBLScrollView.m ├── Rebel-Info.plist ├── CAAnimation+RBLBlockAdditions.m ├── NSView+RBLAlignmentAdditions.m ├── RBLClipView.m ├── RBLScrolling.m ├── Rebel.h ├── NSApplication+RBLBlockAdditions.m ├── NSObject+RBObjectSizzlingAdditions.m ├── NSView+RBLAnimationAdditions.m ├── RBLShadowedTextFieldCell.h ├── NSTextView+RBLAntialiasingAdditions.m ├── NSView+RBLAnimationAdditions.h ├── RBLExpandingContainerView.h ├── RBLShadowedTextFieldCell.m ├── NSFont+RBLFallbackAdditions.m ├── NSColor+RBLCGColorAdditions.m ├── RBLSlidingContainerView.h ├── RBLHTMLView.m ├── RBLExpandingContainerView.m ├── RBLSlidingContainerView.m ├── RBLResizableImage.m ├── RBLPopover.h └── RBLPopover.m ├── Rebel.xcworkspace └── contents.xcworkspacedata ├── README.md ├── CONTRIBUTING.md ├── LICENSE.md └── Rebel.xcodeproj └── xcshareddata └── xcschemes └── Rebel.xcscheme /Demos/RBLPopoverDemo/RBLPopoverDemo/en.lproj/InfoPlist.strings: -------------------------------------------------------------------------------- 1 | /* Localized versions of Info.plist keys */ 2 | 3 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "jspahrsummers/xcconfigs" ~> 0.8 2 | github "Quick/Quick" ~> 0.3 3 | github "Quick/Nimble" ~> 1.0 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v1.0.0" 2 | github "Quick/Quick" "v0.3.1" 3 | github "jspahrsummers/xcconfigs" "0.8.1" 4 | -------------------------------------------------------------------------------- /RebelTests/_testimage.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/Rebel/HEAD/RebelTests/_testimage.jpg -------------------------------------------------------------------------------- /RebelTests/_testimage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/Rebel/HEAD/RebelTests/_testimage.png -------------------------------------------------------------------------------- /RebelTests/_testimage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/github/Rebel/HEAD/RebelTests/_testimage@2x.png -------------------------------------------------------------------------------- /script/schemes.awk: -------------------------------------------------------------------------------- 1 | BEGIN { 2 | FS = "\n"; 3 | } 4 | 5 | /Schemes:/ { 6 | while (getline && $0 != "") { 7 | sub(/^ +/, ""); 8 | print "'" $0 "'"; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /script/targets.awk: -------------------------------------------------------------------------------- 1 | BEGIN { 2 | FS = "\n"; 3 | } 4 | 5 | /Targets:/ { 6 | while (getline && $0 != "") { 7 | if ($0 ~ /Tests/) continue; 8 | 9 | sub(/^ +/, ""); 10 | print "'" $0 "'"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | *.xcworkspace 13 | !default.xcworkspace 14 | xcuserdata 15 | profile 16 | *.moved-aside 17 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/RBLPopoverDemo-Prefix.pch: -------------------------------------------------------------------------------- 1 | // 2 | // Prefix header 3 | // 4 | // The contents of this file are implicitly included at the beginning of every source file. 5 | // 6 | 7 | #ifdef __OBJC__ 8 | #import 9 | #import 10 | #endif 11 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // RBLPopoverDemo 4 | // 5 | // Created by Matt Diephouse on 2/24/14. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | int main(int argc, const char * argv[]) 12 | { 13 | return NSApplicationMain(argc, argv); 14 | } 15 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/RPDContentViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // RPDContentViewController.h 3 | // RBLPopoverDemo 4 | // 5 | // Created by Matt Diephouse on 2/24/14. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RPDContentViewController : NSViewController 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | script: script/cibuild 3 | notifications: 4 | email: false 5 | campfire: 6 | on_success: always 7 | on_failure: always 8 | rooms: 9 | - secure: "QVVzkmaWyLgGMOgtaBF2EUQS3Ot1Bj7jc8CI7L1mqxrDpNnxtemgSNBkvFNrec0RoSMiHBY9t+aVdnHsCLlqhDs3mX55zxlbTJPDpFTVI2p77n90teuOr1kK+a3DYoyaxO6acrAJwhjOe6j9zd3o/su+VG5OsXqCXhBFfnc863w=" 10 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/RPDAppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // RPDAppDelegate.h 3 | // RBLPopoverDemo 4 | // 5 | // Created by Matt Diephouse on 2/24/14. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface RPDAppDelegate : NSObject 12 | 13 | @property (assign) IBOutlet NSWindow *window; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Carthage/Checkouts/Nimble"] 2 | path = Carthage/Checkouts/Nimble 3 | url = https://github.com/Quick/Nimble.git 4 | [submodule "Carthage/Checkouts/Quick"] 5 | path = Carthage/Checkouts/Quick 6 | url = https://github.com/Quick/Quick.git 7 | [submodule "Carthage/Checkouts/xcconfigs"] 8 | url = https://github.com/jspahrsummers/xcconfigs.git 9 | path = Carthage/Checkouts/xcconfigs 10 | -------------------------------------------------------------------------------- /Rebel/RBLTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLTableView.m 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 20/04/2013. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLTableView.h" 10 | #import "RBLScrolling.h" 11 | 12 | @implementation RBLTableView 13 | 14 | - (BOOL)scrollRectToVisible:(NSRect)aRect { 15 | return RBLScrollRectInViewToVisible(self, aRect); 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Rebel/NSObject+RBObjectSizzlingAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+NSObjectSizzlingAdditions.h 3 | // Rebel 4 | // 5 | // Created by Colin Wheeler on 10/29/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSObject (NSObjectSizzlingAdditions) 12 | 13 | + (void)rbl_swapMethod:(SEL)originalSelector with:(SEL)newSelector; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /Rebel/RBLOutlineView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLOutlineView.m 3 | // Rebel 4 | // 5 | // Created by Rob Rix on 26/05/2014. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLOutlineView.h" 10 | #import "RBLScrolling.h" 11 | 12 | @implementation RBLOutlineView 13 | 14 | - (BOOL)scrollRectToVisible:(NSRect)aRect { 15 | return RBLScrollRectInViewToVisible(self, aRect); 16 | } 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /Rebel.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Rebel/RBLScrollView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLScrollView.h 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 12/4/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // A NSScrollView subclass which uses an instance of RBLClipView 12 | // as the clip view instead of NSClipView. 13 | // 14 | // Layer-backed by default. 15 | @interface RBLScrollView : NSScrollView 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /script/xctool.awk: -------------------------------------------------------------------------------- 1 | # Exit statuses: 2 | # 3 | # 0 - No errors found. 4 | # 1 - Wrong SDK. Retry with SDK `iphonesimulator`. 5 | # 2 - Missing target. 6 | 7 | BEGIN { 8 | status = 0; 9 | } 10 | 11 | { 12 | print; 13 | } 14 | 15 | /Testing with the '(.+)' SDK is not yet supported/ { 16 | status = 1; 17 | } 18 | 19 | /does not contain a target named/ { 20 | status = 2; 21 | } 22 | 23 | END { 24 | exit status; 25 | } 26 | -------------------------------------------------------------------------------- /Rebel/NSAttributedString+RBLHTMLAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+RBLHTMLAdditions.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-12-11. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSAttributedString (RBLHTMLAdditions) 12 | 13 | // Returns an attributed string initialized from HTML. 14 | + (instancetype)rbl_attributedStringWithHTML:(NSString *)HTMLString; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/RPDContentViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // RPDContentViewController.m 3 | // RBLPopoverDemo 4 | // 5 | // Created by Matt Diephouse on 2/24/14. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RPDContentViewController.h" 10 | 11 | @implementation RPDContentViewController 12 | 13 | #pragma mark - NSObject 14 | 15 | - (id)init { 16 | return [self initWithNibName:NSStringFromClass(self.class) bundle:nil]; 17 | } 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Rebel/RBLTableCellView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLTableCellView.m 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 10/23/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLTableCellView.h" 10 | 11 | @implementation RBLTableCellView 12 | 13 | - (void)viewDidMoveToSuperview { 14 | if (self.superview == nil) { 15 | [self rbl_prepareForReuse]; 16 | } 17 | 18 | [super viewDidMoveToSuperview]; 19 | } 20 | 21 | - (void)rbl_prepareForReuse { 22 | 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /RebelTests/SwiftSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftSpec.swift 3 | // Archimedes 4 | // 5 | // Created by Justin Spahr-Summers on 2014-10-02. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Nimble 11 | import Quick 12 | 13 | // Without this, the Swift stdlib won't be linked into the test target (even if 14 | // “Embedded Content Contains Swift Code” is enabled). 15 | class SwiftSpec: QuickSpec { 16 | override func spec() { 17 | expect(true).to(beTruthy()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Rebel/RBLHTMLView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLHTMLView.h 3 | // Rebel 4 | // 5 | // Created by Josh Abernathy on 3/13/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | @import WebKit; 10 | 11 | // A view for displaying HTML-styled text. 12 | @interface RBLHTMLView : WebView 13 | 14 | // The CSS to use to style the HTML. 15 | // 16 | // Reasonable defaults are used when this is not set. 17 | @property (nonatomic, copy) NSString *CSS; 18 | 19 | // The HTML to display. 20 | @property (nonatomic, copy) NSString *HTML; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Rebel/NSTextView+RBLAntialiasingAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextView+RBLAntialiasingAdditions.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 10.03.12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | // Portions copyright (c) 2012 Bitswift. All rights reserved. 9 | // See the LICENSE file for more information. 10 | // 11 | 12 | #import 13 | 14 | // This private category fixes blurry text in layer-backed text views and text 15 | // fields. 16 | @interface NSTextView (RBLAntialiasingAdditions) 17 | @end 18 | -------------------------------------------------------------------------------- /Rebel/NSImage+RBLResizableImageAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+RBLResizableImageAdditions.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-10-08. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class RBLResizableImage; 12 | 13 | @interface NSImage (RBLResizableImageAdditions) 14 | 15 | // Returns a new image with the specified cap insets. See RBLResizableImage for 16 | // more information. 17 | - (RBLResizableImage *)rbl_resizableImageWithCapInsets:(NSEdgeInsets)capInsets; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Rebel/NSView+RBLAlignmentAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+RBLAlignmentAdditions.h 3 | // Rebel 4 | // 5 | // Created by Indragie Karunaratne on 2013-03-02. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSView (RBLAlignmentAdditions) 12 | 13 | // Uses `-backingAlignedRect:options:` internally and converts between window coordinates 14 | // and view coordinates. 15 | // 16 | // Returns a backing store pixel aligned rectangle in view coordinates 17 | - (NSRect)rbl_viewBackingAlignedRect:(NSRect)rect options:(NSAlignmentOptions)options; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /Rebel/RBLScrolling.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLScrolling.h 3 | // Rebel 4 | // 5 | // Created by Rob Rix on 27/05/2014. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /// Scrolls `view`’s `enclosingScrollView` just enough to reveal `rect`, 12 | /// rather than scrolling it such that it lies in the middle of the new 13 | /// `visibleRect`. This resolves issues where scrolling quickly through 14 | /// long lists, e.g. by holding down the down arrow key in a table or 15 | /// outline view, will otherwise jump wildly around the scrollable 16 | /// region. 17 | BOOL RBLScrollRectInViewToVisible(NSView *view, NSRect rect); 18 | -------------------------------------------------------------------------------- /Rebel/NSApplication+RBLBlockAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSApplication+RBLBlockAdditions.h 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 10/24/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // Adds support for block handlers on a standard sheet. 12 | @interface NSApplication (RBLBlockAdditions) 13 | 14 | // Adds onto the standard `beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo:` 15 | // with support for a block completion handler. 16 | - (void)rbl_beginSheet:(NSWindow *)sheet modalForWindow:(NSWindow *)modalWindow completionHandler:(void (^)(NSInteger returnCode))handler; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /RebelTests/RBLScrollViewSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLTableView.m 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 12/4/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(RBLScrollViewSpec) 14 | 15 | __block RBLScrollView *scrollView; 16 | 17 | describe(@"clip view", ^{ 18 | beforeEach(^{ 19 | scrollView = [[RBLScrollView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]; 20 | }); 21 | 22 | it(@"should be a RBLClipView", ^{ 23 | expect(scrollView.contentView).to(beAKindOf(RBLClipView.class)); 24 | }); 25 | }); 26 | 27 | QuickSpecEnd 28 | -------------------------------------------------------------------------------- /script/xcodebuild.awk: -------------------------------------------------------------------------------- 1 | # Exit statuses: 2 | # 3 | # 0 - No errors found. 4 | # 1 - Build or test failure. Errors will be logged automatically. 5 | # 2 - Untestable target. Retry with the "build" action. 6 | 7 | BEGIN { 8 | status = 0; 9 | } 10 | 11 | { 12 | print; 13 | fflush(stdout); 14 | } 15 | 16 | /is not valid for Testing/ { 17 | exit 2; 18 | } 19 | 20 | /[0-9]+: (error|warning):/ { 21 | errors = errors $0 "\n"; 22 | } 23 | 24 | /(TEST|BUILD) FAILED/ { 25 | status = 1; 26 | } 27 | 28 | END { 29 | if (length(errors) > 0) { 30 | print "\n*** All errors:\n" errors; 31 | } 32 | 33 | fflush(stdout); 34 | exit status; 35 | } 36 | -------------------------------------------------------------------------------- /Rebel/NSImage+RBLResizableImageAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSImage+RBLResizableImageAdditions.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-10-08. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSImage+RBLResizableImageAdditions.h" 10 | #import "RBLResizableImage.h" 11 | 12 | @implementation NSImage (RBLResizableImageAdditions) 13 | 14 | - (RBLResizableImage *)rbl_resizableImageWithCapInsets:(NSEdgeInsets)capInsets { 15 | RBLResizableImage *image = [[RBLResizableImage alloc] initWithSize:self.size]; 16 | [image addRepresentations:self.representations]; 17 | 18 | image.rbl_capInsets = capInsets; 19 | return image; 20 | } 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Rebel/RBLTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLTableView.h 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 20/04/2013. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // A standard table view with one fix. 12 | // 13 | // As opposed to trying to scroll rects into the middle of the view each time, 14 | // we move them just enough as to make them visible. This fixes the table view 15 | // appearing to have some kind of seizure when you, for example, hold down an 16 | // arrow key to scroll through table cells really fast. 17 | // 18 | // This fix applies to both cell and view based tableviews. 19 | @interface RBLTableView : NSTableView 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Rebel/RBLResizableImage.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLResizableImage.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-07-24. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | // Portions copyright (c) 2011 Twitter. All rights reserved. 9 | // See the LICENSE file for more information. 10 | // 11 | 12 | #import 13 | 14 | // An image that supports resizing based on end caps. 15 | @interface RBLResizableImage : NSImage 16 | 17 | // The end cap insets for the image. 18 | // 19 | // Any portion of the image not covered by end caps will be tiled when the image 20 | // is drawn. 21 | @property (nonatomic, assign) NSEdgeInsets rbl_capInsets; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /Rebel/CAAnimation+RBLBlockAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // CAAnimation+RBLBlockAdditions.h 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 13/09/2012. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface CAAnimation (RBLBlockAdditions) 12 | 13 | // A block called on successful completion of the animation. 14 | // This takes over the delegate property of the animation. If you subsequently 15 | // set a delegate, this will break. It will also replace any existing delegate 16 | // set on the animation. 17 | // 18 | // finished - Whether the animation had finished when it stopped. 19 | @property (nonatomic, copy) void (^rbl_completionBlock)(BOOL finished); 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /Rebel/RBLTableCellView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLTableCellView.h 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 10/23/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // A subclass of NSTableCellView that adds a method 12 | // which notifies when the cell view will be reused. 13 | // Useful to clear properties and bindings before reuse. 14 | @interface RBLTableCellView : NSTableCellView 15 | 16 | 17 | // Called when the cell view has either been removed from 18 | // its superview (the row view), or has just been created. 19 | // 20 | // Either way, the cell will not have a superview during this 21 | // time and will be in an enqueued state. 22 | - (void)rbl_prepareForReuse; 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /Rebel/RBLOutlineView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLOutlineView.h 3 | // Rebel 4 | // 5 | // Created by Rob Rix on 26/05/2014. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | /// A standard outline view with one fix. 12 | /// 13 | /// As opposed to trying to scroll rects into the middle of the view each time, 14 | /// we move them just enough as to make them visible. This fixes the outline 15 | /// view appearing to have some kind of seizure when you, for example, hold 16 | /// down an arrow key to scroll through its cells really fast. 17 | /// 18 | /// This fix applies to both cell and view based outline views. This is the 19 | /// same fix as implemented in `RBLTableView`. 20 | @interface RBLOutlineView : NSOutlineView 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /RebelTests/RebelTests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rebel 2 | 3 | Rebel is a framework to make AppKit easier to work with, using categories to fix bugs and make APIs nicer, and new classes to perform common tasks. 4 | 5 | This framework is very much a work in progress at the moment, and should be considered **alpha quality**. Breaking changes may happen often during this time. 6 | 7 | ## Getting Started 8 | 9 | To start building the framework, clone this repository and then run `script/bootstrap`. 10 | This will automatically pull down any dependencies. 11 | 12 | When working on Rebel in isolation, use the `.xcworkspace` file. When integrating it into another project, use the `.xcodeproj` file. 13 | 14 | ## License 15 | 16 | Rebel is released under the MIT license. See [LICENSE.md](https://github.com/github/Rebel/blob/master/LICENSE.md). 17 | -------------------------------------------------------------------------------- /Rebel/NSColor+RBLCGColorAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+RBLCGColorAdditions.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 01.12.11. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | // Portions copyright (c) 2011 Bitswift. All rights reserved. 9 | // See the LICENSE file for more information. 10 | // 11 | 12 | #import 13 | 14 | // Extensions to NSColor for interoperability with CGColor. 15 | @interface NSColor (RBLCGColorAdditions) 16 | 17 | // The CGColor corresponding to the receiver. 18 | @property (nonatomic, readonly) CGColorRef rbl_CGColor; 19 | 20 | // Returns an NSColor corresponding to the given CGColor. 21 | // 22 | // This currently does not handle pattern colors. 23 | + (NSColor *)rbl_colorWithCGColor:(CGColorRef)color; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Rebel/NSAttributedString+RBLHTMLAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+RBLHTMLAdditions.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-12-11. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSAttributedString+RBLHTMLAdditions.h" 10 | 11 | @implementation NSAttributedString (RBLHTMLAdditions) 12 | 13 | + (instancetype)rbl_attributedStringWithHTML:(NSString *)HTMLString { 14 | NSParameterAssert(HTMLString != nil); 15 | 16 | NSStringEncoding encoding = HTMLString.fastestEncoding; 17 | 18 | NSData *data = [HTMLString dataUsingEncoding:encoding]; 19 | if (data == nil) return nil; 20 | 21 | NSDictionary *options = @{ NSCharacterEncodingDocumentAttribute: @(encoding) }; 22 | return [[self alloc] initWithHTML:data options:options documentAttributes:NULL]; 23 | } 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /Rebel/RBLClipView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLClipView.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-09-14. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | // A faster NSClipView based on CAScrollLayer. 13 | // 14 | // This view should be set as the scroll view's contentView as soon as possible 15 | // after the scroll view is initialized. For some reason, scroll bars will 16 | // disappear on 10.7 (but not 10.8) unless hasHorizontalScroller and 17 | // hasVerticalScroller are set _after_ the contentView. 18 | @interface RBLClipView : NSClipView 19 | 20 | // The backing layer for this view. 21 | @property (atomic, strong) CAScrollLayer *layer; 22 | 23 | // Whether the content in this view is opaque. 24 | // 25 | // Defaults to NO. 26 | @property (atomic, getter = isOpaque) BOOL opaque; 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love that you're interested in contributing to this project! 2 | 3 | To make the process as painless as possible, we have just a couple of guidelines 4 | that should make life easier for everyone involved. 5 | 6 | ## Prefer Pull Requests 7 | 8 | If you know exactly how to implement the feature being suggested or fix the bug 9 | being reported, please open a pull request instead of an issue. Pull requests are easier than 10 | patches or inline code blocks for discussing and merging the changes. 11 | 12 | If you can't make the change yourself, please open an issue after making sure 13 | that one isn't already logged. 14 | 15 | ## Contributing Code 16 | 17 | Fork this repository, make it awesomer (preferably in a branch named for the 18 | topic), send a pull request! 19 | 20 | All code contributions should match our [coding 21 | conventions](https://github.com/github/objective-c-conventions). 22 | 23 | Thanks for contributing! :boom::camel: 24 | -------------------------------------------------------------------------------- /RebelTests/NSView+RBLAlignmentAdditionsSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+RBLAlignmentAdditionsSpec.m 3 | // Rebel 4 | // 5 | // Created by Indragie Karunaratne on 2013-03-22. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(NSViewRBLAlignmentAdditions) 14 | 15 | describe(@"view alignment", ^{ 16 | NSRect nonAlignedRect = NSMakeRect(10.2, 11.8, 12, 13); 17 | NSRect expectedRect = NSMakeRect(10, 12, 12, 13); 18 | 19 | __block NSView *view; 20 | 21 | beforeEach(^{ 22 | view = [[NSView alloc] initWithFrame:NSMakeRect(20, 20, 20, 20)]; 23 | }); 24 | 25 | it(@"should return a rect aligned to the view backing without a window", ^{ 26 | NSRect alignedRect = [view rbl_viewBackingAlignedRect:nonAlignedRect options:NSAlignAllEdgesNearest]; 27 | expect(@(NSEqualRects(alignedRect, expectedRect))).to(beTruthy()); 28 | }); 29 | }); 30 | 31 | QuickSpecEnd 32 | -------------------------------------------------------------------------------- /RebelTests/RBLHTMLViewSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLHTMLViewSpec.m 3 | // Rebel 4 | // 5 | // Created by Josh Abernathy on 3/14/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(RBLHTMLViewSpec) 14 | 15 | static NSString * const HTML = @"hey brother"; 16 | 17 | __block RBLHTMLView *view; 18 | 19 | beforeEach(^{ 20 | view = [[RBLHTMLView alloc] initWithFrame:NSZeroRect]; 21 | view.HTML = HTML; 22 | }); 23 | 24 | it(@"should contain the set HTML", ^{ 25 | expect(@([view.mainFrame.DOMDocument.body.innerHTML rangeOfString:HTML].length)).to(beGreaterThan(@0)); 26 | }); 27 | 28 | it(@"shouldn't be loading after setting the HTML", ^{ 29 | expect(@(view.isLoading)).to(beFalsy()); 30 | }); 31 | 32 | it(@"shouldn't be loading after setting the CSS", ^{ 33 | view.CSS = @"body { color: red; }"; 34 | expect(@(view.isLoading)).to(beFalsy()); 35 | }); 36 | 37 | QuickSpecEnd 38 | -------------------------------------------------------------------------------- /Rebel/NSFont+RBLFallbackAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSFont+RBLFallbackAdditions.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-12-09. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSFont (RBLFallbackAdditions) 12 | 13 | // Looks up a font with the given name and size. 14 | // 15 | // If text rendered in the returned font uses a glyph not present in 16 | // `fontName`, the fonts specified by `fallbackNames` are searched in order, and 17 | // the glyph is rendered in the first font that provides it. 18 | // 19 | // If `fontName` does not exist on the system, `fallbackNames` is searched in 20 | // order for a replacement font. 21 | // 22 | // Returns a font that renders in `fontName` by default, and `fallbackNames` 23 | // only if necessary, or `nil` if no fonts by the given names could be found. 24 | + (NSFont *)rbl_fontWithName:(NSString *)fontName size:(CGFloat)fontSize fallbackNames:(NSArray *)fallbackNames; 25 | 26 | @end 27 | -------------------------------------------------------------------------------- /Rebel/RBLScrollView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLScrollView.m 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 12/4/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLScrollView.h" 10 | #import "RBLClipView.h" 11 | 12 | @implementation RBLScrollView 13 | 14 | #pragma mark Lifecycle 15 | 16 | - (id)initWithFrame:(NSRect)frameRect { 17 | self = [super initWithFrame:frameRect]; 18 | if (self == nil) return nil; 19 | 20 | [self swapClipView]; 21 | 22 | return self; 23 | } 24 | 25 | - (void)awakeFromNib { 26 | [super awakeFromNib]; 27 | 28 | if (![self.contentView isKindOfClass:RBLClipView.class] ) { 29 | [self swapClipView]; 30 | } 31 | } 32 | 33 | #pragma mark Clip view swapping 34 | 35 | - (void)swapClipView { 36 | self.wantsLayer = YES; 37 | id documentView = self.documentView; 38 | RBLClipView *clipView = [[RBLClipView alloc] initWithFrame:self.contentView.frame]; 39 | self.contentView = clipView; 40 | self.documentView = documentView; 41 | } 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /Rebel/Rebel-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | FMWK 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | NSHumanReadableCopyright 26 | Copyright © 2012 GitHub. All rights reserved. 27 | NSPrincipalClass 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Rebel/CAAnimation+RBLBlockAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // CAAnimation+RBLBlockAdditions.m 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 13/09/2012. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "CAAnimation+RBLBlockAdditions.h" 10 | 11 | @interface RBLCAAnimationDelegate : NSObject 12 | @property (nonatomic, copy) void (^completion)(BOOL finished); 13 | @end 14 | 15 | @implementation CAAnimation (RBLBlockAdditions) 16 | 17 | - (void)setRbl_completionBlock:(void (^)(BOOL))block { 18 | RBLCAAnimationDelegate *stub = [[RBLCAAnimationDelegate alloc] init]; 19 | stub.completion = block; 20 | self.delegate = stub; 21 | } 22 | 23 | - (void (^)(BOOL))rbl_completionBlock { 24 | return ([self.delegate isKindOfClass:RBLCAAnimationDelegate.class] ? [(RBLCAAnimationDelegate *)self.delegate completion] : nil); 25 | } 26 | 27 | @end 28 | 29 | @implementation RBLCAAnimationDelegate 30 | 31 | - (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag { 32 | if (self.completion != nil) { 33 | self.completion(flag); 34 | } 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /script/LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2013 Justin Spahr-Summers** 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Rebel/NSView+RBLAlignmentAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+RBLAlignmentAdditions.m 3 | // Rebel 4 | // 5 | // Created by Indragie Karunaratne on 2013-03-02. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSView+RBLAlignmentAdditions.h" 10 | 11 | @implementation NSView (RBLAlignmentAdditions) 12 | 13 | - (NSRect)rbl_viewBackingAlignedRect:(NSRect)rect options:(NSAlignmentOptions)options { 14 | if (self.window != nil) { 15 | NSRect windowRect = [self convertRect:rect toView:nil]; 16 | NSRect windowBackingRect = [self backingAlignedRect:windowRect options:options]; 17 | return [self convertRect:windowBackingRect fromView:nil]; 18 | } else { 19 | // Use a best guess for how to align to the backing store. 20 | CGFloat scaleFactor = NSScreen.mainScreen.backingScaleFactor; 21 | CGAffineTransform transformToBacking = CGAffineTransformMakeScale(scaleFactor, scaleFactor); 22 | 23 | CGRect backingRect = CGRectApplyAffineTransform(rect, transformToBacking); 24 | backingRect = NSIntegralRectWithOptions(backingRect, options); 25 | return CGRectApplyAffineTransform(backingRect, CGAffineTransformInvert(transformToBacking)); 26 | } 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Rebel/RBLClipView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLClipView.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-09-14. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLClipView.h" 10 | #import "NSColor+RBLCGColorAdditions.h" 11 | 12 | @implementation RBLClipView 13 | 14 | #pragma mark Properties 15 | 16 | @dynamic layer; 17 | 18 | - (NSColor *)backgroundColor { 19 | return [NSColor rbl_colorWithCGColor:self.layer.backgroundColor]; 20 | } 21 | 22 | - (void)setBackgroundColor:(NSColor *)color { 23 | self.layer.backgroundColor = color.rbl_CGColor; 24 | } 25 | 26 | - (BOOL)isOpaque { 27 | return self.layer.opaque; 28 | } 29 | 30 | - (void)setOpaque:(BOOL)opaque { 31 | self.layer.opaque = opaque; 32 | } 33 | 34 | #pragma mark Lifecycle 35 | 36 | - (id)initWithFrame:(NSRect)frame { 37 | self = [super initWithFrame:frame]; 38 | if (self == nil) return nil; 39 | 40 | self.layer = [CAScrollLayer layer]; 41 | self.wantsLayer = YES; 42 | 43 | self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawNever; 44 | 45 | // Matches default NSClipView settings. 46 | self.backgroundColor = NSColor.clearColor; 47 | self.opaque = NO; 48 | 49 | return self; 50 | } 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "mac", 5 | "size" : "16x16", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "mac", 10 | "size" : "16x16", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "mac", 15 | "size" : "32x32", 16 | "scale" : "1x" 17 | }, 18 | { 19 | "idiom" : "mac", 20 | "size" : "32x32", 21 | "scale" : "2x" 22 | }, 23 | { 24 | "idiom" : "mac", 25 | "size" : "128x128", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "mac", 35 | "size" : "256x256", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "mac", 40 | "size" : "256x256", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "mac", 45 | "size" : "512x512", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "mac", 50 | "size" : "512x512", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /Rebel/RBLScrolling.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLScrolling.m 3 | // Rebel 4 | // 5 | // Created by Rob Rix on 27/05/2014. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLScrolling.h" 10 | 11 | @interface NSScrollView (RBLScrolling1010APIs) 12 | 13 | @property NSEdgeInsets contentInsets; 14 | 15 | @end 16 | 17 | BOOL RBLScrollRectInViewToVisible(NSView *view, NSRect rect) { 18 | NSScrollView *scrollView = view.enclosingScrollView; 19 | NSRect visibleRect = view.visibleRect; 20 | 21 | NSEdgeInsets insets = NSEdgeInsetsMake(0, 0, 0, 0); 22 | if ([scrollView respondsToSelector:@selector(contentInsets)]) { 23 | insets = scrollView.contentInsets; 24 | } 25 | 26 | void (^scrollToY)(CGFloat) = ^(CGFloat y) { 27 | NSPoint pointToScrollTo = NSMakePoint(0, y); 28 | 29 | [scrollView.contentView scrollToPoint:pointToScrollTo]; 30 | [scrollView reflectScrolledClipView:scrollView.contentView]; 31 | }; 32 | 33 | if (NSMinY(rect) < NSMinY(visibleRect)) { 34 | scrollToY(NSMinY(rect) - insets.top); 35 | return YES; 36 | } 37 | 38 | if (NSMaxY(rect) > NSMaxY(visibleRect)) { 39 | scrollToY(NSMaxY(rect) - NSHeight(visibleRect) + insets.bottom); 40 | return YES; 41 | } 42 | 43 | return NO; 44 | } 45 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/RBLPopoverDemo-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | com.github.${PRODUCT_NAME:rfc1034identifier} 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | ${PRODUCT_NAME} 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSMinimumSystemVersion 26 | ${MACOSX_DEPLOYMENT_TARGET} 27 | NSHumanReadableCopyright 28 | Copyright © 2014 GitHub. All rights reserved. 29 | NSMainNibFile 30 | MainMenu 31 | NSPrincipalClass 32 | NSApplication 33 | 34 | 35 | -------------------------------------------------------------------------------- /Rebel/Rebel.h: -------------------------------------------------------------------------------- 1 | // 2 | // Rebel.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-07-29. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for Rebel. 12 | FOUNDATION_EXPORT double RebelVersionNumber; 13 | 14 | //! Project version string for Rebel. 15 | FOUNDATION_EXPORT const unsigned char RebelVersionString[]; 16 | 17 | #import 18 | #import 19 | #import 20 | #import 21 | #import 22 | #import 23 | #import 24 | #import 25 | #import 26 | #import 27 | #import 28 | #import 29 | #import 30 | #import 31 | #import 32 | #import 33 | #import 34 | #import 35 | #import 36 | #import 37 | -------------------------------------------------------------------------------- /Rebel/NSApplication+RBLBlockAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSApplication+RBLBlockAdditions.m 3 | // Rebel 4 | // 5 | // Created by Jonathan Willing on 10/24/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSApplication+RBLBlockAdditions.h" 10 | #import 11 | 12 | static void *RBLNSApplicationSheetBlockAssociatedObjectKey = &RBLNSApplicationSheetBlockAssociatedObjectKey; 13 | 14 | @implementation NSApplication (RBLBlockAdditions) 15 | 16 | - (void)rbl_beginSheet:(NSWindow *)sheet modalForWindow:(NSWindow *)modalWindow completionHandler:(void (^)(NSInteger returnCode))handler { 17 | [self beginSheet:sheet modalForWindow:modalWindow modalDelegate:self didEndSelector:@selector(rbl_sheetDidEnd:returnCode:contextInfo:) contextInfo:NULL]; 18 | objc_setAssociatedObject(sheet, RBLNSApplicationSheetBlockAssociatedObjectKey, handler, OBJC_ASSOCIATION_COPY_NONATOMIC); 19 | } 20 | 21 | - (void)rbl_sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { 22 | void (^handler)(NSInteger returnCode) = objc_getAssociatedObject(sheet, RBLNSApplicationSheetBlockAssociatedObjectKey); 23 | [sheet orderOut:self]; 24 | handler(returnCode); 25 | objc_setAssociatedObject(sheet, RBLNSApplicationSheetBlockAssociatedObjectKey, nil, OBJC_ASSOCIATION_COPY_NONATOMIC); 26 | } 27 | 28 | @end 29 | -------------------------------------------------------------------------------- /Rebel/NSObject+RBObjectSizzlingAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSObject+NSObjectSizzlingAdditions.m 3 | // Rebel 4 | // 5 | // Created by Colin Wheeler on 10/29/12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSObject+RBObjectSizzlingAdditions.h" 10 | #import 11 | 12 | @implementation NSObject (NSObjectSizzlingAdditions) 13 | 14 | // Shamelessly taken from JAViewController since mine is a bit longer & ties into my other framework methods 15 | + (void)rbl_swapMethod:(SEL)originalSelector with:(SEL)newSelector { 16 | Method originalMethod = class_getInstanceMethod(self, originalSelector); 17 | Method newMethod = class_getInstanceMethod(self, newSelector); 18 | const char *originalTypeEncoding = method_getTypeEncoding(originalMethod); 19 | const char *newTypeEncoding = method_getTypeEncoding(newMethod); 20 | NSAssert2(!strcmp(originalTypeEncoding, newTypeEncoding), @"Method type encodings must be the same: %s vs. %s", originalTypeEncoding, newTypeEncoding); 21 | 22 | if(class_addMethod(self, originalSelector, method_getImplementation(newMethod), newTypeEncoding)) { 23 | class_replaceMethod(self, newSelector, method_getImplementation(originalMethod), originalTypeEncoding); 24 | } else { 25 | method_exchangeImplementations(originalMethod, newMethod); 26 | } 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Rebel/NSView+RBLAnimationAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+RBLAnimationAdditions.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-09-04. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSView+RBLAnimationAdditions.h" 10 | 11 | static NSUInteger RBLAnimationContextCount = 0; 12 | 13 | @implementation NSView (RBLAnimationAdditions) 14 | 15 | + (void)rbl_animate:(void (^)(void))animations { 16 | [self rbl_animate:animations completion:nil]; 17 | } 18 | 19 | + (void)rbl_animate:(void (^)(void))animations completion:(void (^)(void))completion { 20 | // It's not clear whether NSAnimationContext will accept a nil completion 21 | // block. 22 | if (completion == nil) completion = ^{}; 23 | 24 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) { 25 | RBLAnimationContextCount++; 26 | animations(); 27 | RBLAnimationContextCount--; 28 | } completionHandler:completion]; 29 | } 30 | 31 | + (void)rbl_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(void))completion { 32 | [self rbl_animate:^{ 33 | NSAnimationContext.currentContext.duration = duration; 34 | animations(); 35 | } completion:completion]; 36 | } 37 | 38 | + (BOOL)rbl_isInAnimationContext { 39 | return RBLAnimationContextCount > 0; 40 | } 41 | 42 | - (instancetype)rbl_animator { 43 | return self.class.rbl_isInAnimationContext ? self.animator : self; 44 | } 45 | 46 | @end 47 | -------------------------------------------------------------------------------- /Rebel/RBLShadowedTextFieldCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLShadowedTextFieldCell.h 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 18/02/2013. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // Use this value to set a shadow to be used as a fallback for all background 12 | // styles. 13 | // 14 | // If a shadow is explicitly set for a particular background style, it will be 15 | // preferred to any shadows set to affect all background styles. 16 | extern NSBackgroundStyle const RBLShadowedTextFieldAllBackgroundStyles; 17 | 18 | // A text field cell subclass that allows easy drawing of shadows on text. A 19 | // different shadow can be set for each background style of the cell. 20 | // 21 | // An example use case here is labels within table views. 22 | @interface RBLShadowedTextFieldCell : NSTextFieldCell 23 | 24 | // Sets a shadow to be drawn whenever the cell has the provided background style 25 | // 26 | // shadow - The shadow to use for the given `backgroundStyle`. If nil 27 | // it removes any previously set shadow. 28 | // backgroundStyle - The background style for which the given shadow should be 29 | // drawn, or `RBLShadowedTextFieldAllBackgroundStyles` if the 30 | // shadow should be used as a fallback for all background 31 | // styles. 32 | - (void)setShadow:(NSShadow *)shadow forBackgroundStyle:(NSBackgroundStyle)backgroundStyle; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /RebelTests/NSAttributedString+RBLHTMLAdditionsSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSAttributedString+RBLHTMLAdditionsSpec.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-12-11. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(NSAttributedStringHTMLAdditions) 14 | 15 | it(@"should initialize from a simple HTML string", ^{ 16 | NSAttributedString *attributedString = [NSAttributedString rbl_attributedStringWithHTML:@"some formatted
text"]; 17 | expect(attributedString).notTo(beNil()); 18 | 19 | // within "some" 20 | expect([attributedString attributesAtIndex:1 effectiveRange:NULL][NSUnderlineStyleAttributeName]).to(beNil()); 21 | 22 | // within "formatted" 23 | expect([attributedString attributesAtIndex:7 effectiveRange:NULL][NSUnderlineStyleAttributeName]).to(equal(@(NSUnderlineStyleSingle))); 24 | 25 | expect(@([attributedString.string characterAtIndex:14])).to(equal(@('\n'))); 26 | }); 27 | 28 | it(@"should initialize from an HTML string with CSS styling", ^{ 29 | NSAttributedString *attributedString = [NSAttributedString rbl_attributedStringWithHTML:@"formatted text"]; 30 | expect(attributedString).notTo(beNil()); 31 | 32 | NSFont *font = [attributedString attributesAtIndex:1 effectiveRange:NULL][NSFontAttributeName]; 33 | expect(font).notTo(beNil()); 34 | expect(font.familyName).to(equal(@"Courier")); 35 | }); 36 | 37 | QuickSpecEnd 38 | -------------------------------------------------------------------------------- /Rebel/NSTextView+RBLAntialiasingAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextView+RBLAntialiasingAdditions.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 10.03.12. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | // Portions copyright (c) 2012 Bitswift. All rights reserved. 9 | // See the LICENSE file for more information. 10 | // 11 | 12 | #import "NSTextView+RBLAntialiasingAdditions.h" 13 | #import "NSView+RBLAlignmentAdditions.h" 14 | #import 15 | 16 | static void (*originalDrawRectIMP)(id, SEL, NSRect); 17 | 18 | static void fixedDrawRect (NSTextView *self, SEL _cmd, NSRect rect) { 19 | CGContextRef context = NSGraphicsContext.currentContext.graphicsPort; 20 | 21 | CGContextSetAllowsAntialiasing(context, YES); 22 | CGContextSetAllowsFontSmoothing(context, YES); 23 | CGContextSetAllowsFontSubpixelPositioning(context, YES); 24 | CGContextSetAllowsFontSubpixelQuantization(context, YES); 25 | 26 | if (self.superview) { 27 | // NSTextView likes to fall on non-integral points sometimes -- fix 28 | // that. 29 | self.frame = [self.superview rbl_viewBackingAlignedRect:self.frame options:NSAlignAllEdgesNearest]; 30 | } 31 | 32 | originalDrawRectIMP(self, _cmd, rect); 33 | } 34 | 35 | @implementation NSTextView (RBLAntialiasingAdditions) 36 | 37 | + (void)load { 38 | Method drawRect = class_getInstanceMethod(self, @selector(drawRect:)); 39 | originalDrawRectIMP = (void (*)(id, SEL, NSRect))method_getImplementation(drawRect); 40 | 41 | method_setImplementation(drawRect, (IMP)&fixedDrawRect); 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /Rebel/NSView+RBLAnimationAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+RBLAnimationAdditions.h 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-09-04. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // Better block-based animation and animator proxies. 12 | @interface NSView (RBLAnimationAdditions) 13 | 14 | // Invokes +rbl_animate:completion: with a nil completion block. 15 | + (void)rbl_animate:(void (^)(void))animations; 16 | 17 | // Executes the given animation block within a new NSAnimationContext. When all 18 | // animations in the group complete or are canceled, the given completion block 19 | // (if not nil) will be invoked. 20 | + (void)rbl_animate:(void (^)(void))animations completion:(void (^)(void))completion; 21 | 22 | // Invokes +rbl_animate:completion:, setting the animation duration of the 23 | // context before queuing the given animations. 24 | + (void)rbl_animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(void))completion; 25 | 26 | // Returns whether the calling code is executing in an animation context created 27 | // by this category (like through -rbl_animate:completion:). 28 | // 29 | // This only describes whether an animation context is open, not if animations 30 | // happen to be executing at the moment. 31 | + (BOOL)rbl_isInAnimationContext; 32 | 33 | // Returns an animator proxy for the receiver if +rbl_isInAnimationContext 34 | // returns YES. Otherwise, the receiver is returned (so that animating is 35 | // effectively disabled). 36 | - (instancetype)rbl_animator; 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SCRIPT_DIR=$(dirname "$0") 4 | 5 | ## 6 | ## Configuration Variables 7 | ## 8 | 9 | config () 10 | { 11 | # A whitespace-separated list of executables that must be present and locatable. 12 | : ${REQUIRED_TOOLS="xctool"} 13 | 14 | export REQUIRED_TOOLS 15 | } 16 | 17 | ## 18 | ## Bootstrap Process 19 | ## 20 | 21 | main () 22 | { 23 | config 24 | 25 | if [ -n "$REQUIRED_TOOLS" ] 26 | then 27 | echo "*** Checking dependencies..." 28 | check_deps 29 | fi 30 | 31 | local submodules=$(git submodule status) 32 | local result=$? 33 | 34 | if [ "$result" -ne "0" ] 35 | then 36 | exit $result 37 | fi 38 | 39 | if [ -n "$submodules" ] 40 | then 41 | echo "*** Updating submodules..." 42 | update_submodules 43 | fi 44 | } 45 | 46 | check_deps () 47 | { 48 | for tool in $REQUIRED_TOOLS 49 | do 50 | which -s "$tool" 51 | if [ "$?" -ne "0" ] 52 | then 53 | echo "*** Error: $tool not found. Please install it and bootstrap again." 54 | exit 1 55 | fi 56 | done 57 | } 58 | 59 | bootstrap_submodule () 60 | { 61 | local bootstrap="script/bootstrap" 62 | 63 | if [ -e "$bootstrap" ] 64 | then 65 | echo "*** Bootstrapping $name..." 66 | "$bootstrap" >/dev/null 67 | else 68 | update_submodules 69 | fi 70 | } 71 | 72 | update_submodules () 73 | { 74 | git submodule sync --quiet && git submodule update --init && git submodule foreach --quiet bootstrap_submodule 75 | } 76 | 77 | export -f bootstrap_submodule 78 | export -f update_submodules 79 | 80 | main 81 | -------------------------------------------------------------------------------- /RebelTests/NSFont+RBLFallbackAdditionsSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSFont+RBLFallbackAdditionsSpec.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-12-09. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(NSFontRBLFallbackAdditions) 14 | 15 | describe(@"+rbl_fontWithName:size:fallbackNames:", ^{ 16 | __block NSFont *font; 17 | 18 | afterEach(^{ 19 | // All of the below tests should return Helvetica size 12. 20 | expect(font).notTo(beNil()); 21 | expect(font.familyName).to(equal(@"Helvetica")); 22 | expect(@(font.pointSize)).to(equal(@12)); 23 | }); 24 | 25 | it(@"should return a valid font without any fallback names", ^{ 26 | font = [NSFont rbl_fontWithName:@"Helvetica" size:12 fallbackNames:@[]]; 27 | }); 28 | 29 | it(@"should return a valid font with fallback names", ^{ 30 | font = [NSFont rbl_fontWithName:@"Helvetica" size:12 fallbackNames:@[ @"Lucida Grande" ]]; 31 | }); 32 | 33 | it(@"should return a fallback font if the desired font couldn't be found", ^{ 34 | font = [NSFont rbl_fontWithName:@"somemadeupfontname" size:12 fallbackNames:@[ @"Helvetica" ]]; 35 | }); 36 | 37 | it(@"should return the second fallback font if the first two desired fonts couldn't be found", ^{ 38 | font = [NSFont rbl_fontWithName:@"somemadeupfontname" size:12 fallbackNames:@[ @"anothermadeupfontname", @"Helvetica" ]]; 39 | }); 40 | 41 | it(@"should search the fallback font list in order", ^{ 42 | font = [NSFont rbl_fontWithName:@"somemadeupfontname" size:12 fallbackNames:@[ @"Helvetica", @"Lucida Grande" ]]; 43 | }); 44 | }); 45 | 46 | QuickSpecEnd 47 | -------------------------------------------------------------------------------- /Rebel/RBLExpandingContainerView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLExpandingContainerView.h 3 | // Rebel 4 | // 5 | // Created by Alan Rogers on 4/02/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // A view class that uses Auto Layout to adjust its bounds to fit a contentView, 12 | // and optionally animate this change. During the bounds change, the contentView 13 | // is pinned to the top left. 14 | // 15 | // NOTE: You must use to the provided -setContentView: methods to change the 16 | // subview. Calling any of the normal subview mutating methods on the receiver 17 | // will result in undefined behavior. 18 | // IMPORTANT NOTE: Auto Layout will be enabled for any NSWindow you add this 19 | // view to. 20 | @interface RBLExpandingContainerView : NSView 21 | 22 | // The contentView (single subview) of the receiver. 23 | // 24 | // Setting this property is equivalent to calling 25 | // -setContentView:animated:completion: with animations disabled and 26 | // a nil completion block. 27 | @property (nonatomic, strong) NSView *contentView; 28 | 29 | // This method will replace the receiver's only subview with contentView. 30 | // 31 | // contentView - The view to be contained in the receiver. 32 | // animated - If YES, the receiver's bounds will animate to fit the new 33 | // contentView (pinned to the top left). 34 | // completion - An optional block to invoke upon completion of the animation. 35 | // May be nil. 36 | - (void)setContentView:(NSView *)contentView animated:(BOOL)animated completion:(void (^)(void))completionBlock; 37 | 38 | // Calls -setContentView:animated:completion: with a nil completionBlock. 39 | - (void)setContentView:(NSView *)contentView animated:(BOOL)animated; 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /RebelTests/CAAnimation+RBLBlockAdditionsSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+RBLCGColorAdditionsSpec.m 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 2012-09-13. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(CAAnimationRBLBlockAdditions) 14 | 15 | __block CAAnimation *animation = nil; 16 | __block BOOL completionExecuted = NO; 17 | __block void (^completionBlock)(BOOL) = ^(BOOL finished) { 18 | completionExecuted = YES; 19 | }; 20 | 21 | beforeEach(^{ 22 | animation = [[CAAnimation alloc] init]; 23 | animation.rbl_completionBlock = completionBlock; 24 | 25 | expect(animation).notTo(beNil()); 26 | }); 27 | 28 | it(@"Should have set a completion block", ^ { 29 | expect(animation.rbl_completionBlock).notTo(beNil()); 30 | expect(animation.rbl_completionBlock).to(equal(completionBlock)); 31 | if (animation.rbl_completionBlock != nil) animation.rbl_completionBlock(YES); 32 | expect(@(completionExecuted)).to(beTruthy()); 33 | }); 34 | 35 | it(@"Should not have a nil delegate", ^{ 36 | expect(animation.delegate).notTo(beNil()); 37 | }); 38 | 39 | it(@"Should fire once animation is completed", ^{ 40 | CALayer *layer = [CALayer layer]; 41 | CABasicAnimation *sampleAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; 42 | __block BOOL fired = NO; 43 | sampleAnimation.rbl_completionBlock = ^(BOOL complete) { 44 | fired = YES; 45 | 46 | }; 47 | sampleAnimation.duration = 0.0; 48 | [layer addAnimation:sampleAnimation forKey:@"opacity"]; 49 | layer.opacity = 1.f; 50 | expect(@(fired)).toEventually(beTruthy()); 51 | }); 52 | 53 | it(@"Should return nil if no completion block has been set", ^{ 54 | CAAnimation *animation = [CAAnimation animation]; 55 | expect(animation.rbl_completionBlock).to(beNil()); 56 | }); 57 | 58 | 59 | QuickSpecEnd 60 | -------------------------------------------------------------------------------- /RebelTests/NSView+RBLAnimationAdditionsSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSView+RBLAnimationAdditionsSpec.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-09-04. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(NSViewRBLAnimationAdditions) 14 | 15 | describe(@"animation contexts", ^{ 16 | it(@"should not be in an animation context by default", ^{ 17 | expect(@(NSView.rbl_isInAnimationContext)).to(beFalsy()); 18 | }); 19 | 20 | it(@"should not be in an animation context within a new NSAnimationContext", ^{ 21 | [NSAnimationContext beginGrouping]; 22 | expect(@(NSView.rbl_isInAnimationContext)).to(beFalsy()); 23 | [NSAnimationContext endGrouping]; 24 | 25 | [NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){ 26 | expect(@(NSView.rbl_isInAnimationContext)).to(beFalsy()); 27 | } completionHandler:^{}]; 28 | }); 29 | 30 | it(@"should be in an animation context within a Rebel block-based animation", ^{ 31 | [NSView rbl_animate:^{ 32 | expect(@(NSView.rbl_isInAnimationContext)).to(beTruthy()); 33 | 34 | [NSView rbl_animate:^{ 35 | expect(@(NSView.rbl_isInAnimationContext)).to(beTruthy()); 36 | } completion:nil]; 37 | 38 | expect(@(NSView.rbl_isInAnimationContext)).to(beTruthy()); 39 | } completion:nil]; 40 | 41 | expect(@(NSView.rbl_isInAnimationContext)).to(beFalsy()); 42 | }); 43 | }); 44 | 45 | describe(@"animator proxies", ^{ 46 | NSView *view = [[NSView alloc] initWithFrame:NSZeroRect]; 47 | 48 | it(@"should not return an animator proxy outside of an animation context", ^{ 49 | // to.beEqual() will invoke -isEqual:, which might pass even if they're 50 | // not identical. 51 | expect(@(view.rbl_animator == view)).to(beTruthy()); 52 | }); 53 | 54 | it(@"should return an animator proxy within an animation context", ^{ 55 | [NSView rbl_animate:^{ 56 | expect(@(view.rbl_animator == view)).to(beFalsy()); 57 | } completion:nil]; 58 | }); 59 | }); 60 | 61 | QuickSpecEnd 62 | -------------------------------------------------------------------------------- /Rebel/RBLShadowedTextFieldCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLShadowedTextFieldCell.m 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 18/02/2013. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLShadowedTextFieldCell.h" 10 | 11 | #import "NSColor+RBLCGColorAdditions.h" 12 | 13 | NSBackgroundStyle const RBLShadowedTextFieldAllBackgroundStyles = 0xFFFFFFFF; 14 | 15 | @interface RBLShadowedTextFieldCell () 16 | 17 | // Maps keys of backgroundStyles to values of shadows. 18 | @property (nonatomic, strong) NSMutableDictionary *backgroundStylesToShadows; 19 | 20 | @end 21 | 22 | @implementation RBLShadowedTextFieldCell 23 | 24 | #pragma mark Lifecycle 25 | 26 | static void CommonInit(RBLShadowedTextFieldCell *textFieldCell) { 27 | textFieldCell.backgroundStylesToShadows = [NSMutableDictionary dictionary]; 28 | } 29 | 30 | - (instancetype)initWithCoder:(NSCoder *)aDecoder { 31 | self = [super initWithCoder:aDecoder]; 32 | if (self == nil) return nil; 33 | 34 | CommonInit(self); 35 | 36 | return self; 37 | } 38 | 39 | - (instancetype)initTextCell:(NSString *)aString { 40 | self = [super initTextCell:aString]; 41 | if (self == nil) return nil; 42 | 43 | CommonInit(self); 44 | 45 | return self; 46 | } 47 | 48 | #pragma mark Drawing 49 | 50 | - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView { 51 | [NSGraphicsContext saveGraphicsState]; 52 | NSShadow *shadow = self.backgroundStylesToShadows[@(self.backgroundStyle)]; 53 | if (shadow == nil) { 54 | shadow = self.backgroundStylesToShadows[@(RBLShadowedTextFieldAllBackgroundStyles)]; 55 | } 56 | 57 | if (shadow != nil) { 58 | CGContextSetShadowWithColor(NSGraphicsContext.currentContext.graphicsPort, shadow.shadowOffset, shadow.shadowBlurRadius, shadow.shadowColor.rbl_CGColor); 59 | } 60 | 61 | [super drawWithFrame:cellFrame inView:controlView]; 62 | 63 | [NSGraphicsContext restoreGraphicsState]; 64 | } 65 | 66 | #pragma mark API 67 | 68 | - (void)setShadow:(NSShadow *)shadow forBackgroundStyle:(NSBackgroundStyle)backgroundStyle { 69 | if (shadow == nil) { 70 | [self.backgroundStylesToShadows removeObjectForKey:@(backgroundStyle)]; 71 | return; 72 | } 73 | 74 | self.backgroundStylesToShadows[@(backgroundStyle)] = shadow; 75 | } 76 | 77 | @end 78 | -------------------------------------------------------------------------------- /Rebel/NSFont+RBLFallbackAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSFont+RBLFallbackAdditions.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-12-09. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "NSFont+RBLFallbackAdditions.h" 10 | 11 | @implementation NSFont (RBLFallbackAdditions) 12 | 13 | + (NSFont *)rbl_fontWithName:(NSString *)fontName size:(CGFloat)fontSize fallbackNames:(NSArray *)fallbackNames { 14 | NSParameterAssert(fontName != nil); 15 | 16 | NSSet *mandatoryKeys = [NSSet setWithObjects:NSFontNameAttribute, NSFontSizeAttribute, nil]; 17 | NSMutableArray *fallbackDescriptors = [NSMutableArray arrayWithCapacity:fallbackNames.count]; 18 | 19 | for (NSString *fallbackName in fallbackNames) { 20 | // Our ideal fallback font. 21 | NSFontDescriptor *searchDescriptor = [NSFontDescriptor fontDescriptorWithName:fallbackName size:fontSize]; 22 | 23 | // Now check to see whether a match actually exists, falling back to 24 | // a different size if necessary. 25 | NSFontDescriptor *matchingDescriptor = [searchDescriptor matchingFontDescriptorWithMandatoryKeys:mandatoryKeys]; 26 | if (matchingDescriptor == nil) continue; 27 | 28 | [fallbackDescriptors addObject:matchingDescriptor]; 29 | } 30 | 31 | NSMutableArray *remainingFontNames = [fallbackNames mutableCopy]; 32 | NSAssert(fallbackDescriptors.count <= remainingFontNames.count, @"Should have no more fallback font descriptors (%lu) than names to try (%lu)", (unsigned long)fallbackDescriptors.count, (unsigned long)remainingFontNames.count); 33 | 34 | NSMutableDictionary *attributes = [NSMutableDictionary dictionary]; 35 | if (fallbackDescriptors.count > 0) attributes[NSFontCascadeListAttribute] = fallbackDescriptors; 36 | 37 | while (YES) { 38 | attributes[NSFontNameAttribute] = fontName; 39 | 40 | NSFont *font = [NSFont fontWithDescriptor:[NSFontDescriptor fontDescriptorWithFontAttributes:attributes] size:fontSize]; 41 | if (font != nil) return font; 42 | 43 | if (remainingFontNames.count == 0) break; 44 | 45 | if (fallbackDescriptors.count > 0) { 46 | NSString *topName = [fallbackDescriptors[0] fontAttributes][NSFontAttributeName]; 47 | if ([topName isEqual:fontName]) [fallbackDescriptors removeObjectAtIndex:0]; 48 | } 49 | 50 | // Try the next font in the list. 51 | fontName = remainingFontNames[0]; 52 | [remainingFontNames removeObjectAtIndex:0]; 53 | } 54 | 55 | return nil; 56 | } 57 | 58 | @end 59 | -------------------------------------------------------------------------------- /Rebel/NSColor+RBLCGColorAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+RBLCGColorAdditions.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 01.12.11. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | // Portions copyright (c) 2011 Bitswift. All rights reserved. 9 | // See the LICENSE file for more information. 10 | // 11 | 12 | #import "NSColor+RBLCGColorAdditions.h" 13 | 14 | static void drawCGImagePattern (void *info, CGContextRef context) { 15 | CGImageRef image = info; 16 | 17 | size_t width = CGImageGetWidth(image); 18 | size_t height = CGImageGetHeight(image); 19 | 20 | CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); 21 | } 22 | 23 | static void releasePatternInfo (void *info) { 24 | CFRelease(info); 25 | } 26 | 27 | @implementation NSColor (RBLCGColorAdditions) 28 | 29 | + (NSColor *)rbl_colorWithCGColor:(CGColorRef)color { 30 | if (color == nil) { 31 | return nil; 32 | } 33 | 34 | CGColorSpaceRef colorSpaceRef = CGColorGetColorSpace(color); 35 | 36 | NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithCGColorSpace:colorSpaceRef]; 37 | NSColor *result = [self colorWithColorSpace:colorSpace components:CGColorGetComponents(color) count:(size_t)CGColorGetNumberOfComponents(color)]; 38 | [colorSpace release]; 39 | 40 | return result; 41 | } 42 | 43 | - (CGColorRef)rbl_CGColor { 44 | if ([self.colorSpaceName isEqualToString:NSPatternColorSpace]) { 45 | CGImageRef patternImage = [self.patternImage CGImageForProposedRect:NULL context:nil hints:nil]; 46 | if (patternImage == NULL) { 47 | return NULL; 48 | } 49 | 50 | size_t width = CGImageGetWidth(patternImage); 51 | size_t height = CGImageGetHeight(patternImage); 52 | 53 | CGRect patternBounds = CGRectMake(0, 0, width, height); 54 | CGPatternRef pattern = CGPatternCreate( 55 | // Released in releasePatternInfo(). 56 | (void *)CFRetain(patternImage), 57 | patternBounds, 58 | CGAffineTransformIdentity, 59 | width, 60 | height, 61 | kCGPatternTilingConstantSpacingMinimalDistortion, 62 | YES, 63 | &(CGPatternCallbacks){ 64 | .version = 0, 65 | .drawPattern = &drawCGImagePattern, 66 | .releaseInfo = &releasePatternInfo 67 | } 68 | ); 69 | 70 | CGColorSpaceRef colorSpaceRef = CGColorSpaceCreatePattern(NULL); 71 | 72 | CGColorRef result = CGColorCreateWithPattern(colorSpaceRef, pattern, (CGFloat[]){ 1.0 }); 73 | 74 | CGColorSpaceRelease(colorSpaceRef); 75 | CGPatternRelease(pattern); 76 | 77 | return (CGColorRef)[(id)result autorelease]; 78 | } 79 | 80 | NSColorSpace *colorSpace = NSColorSpace.genericRGBColorSpace; 81 | NSColor *color = [self colorUsingColorSpace:colorSpace]; 82 | 83 | CGFloat components[color.numberOfComponents]; 84 | [color getComponents:components]; 85 | 86 | CGColorSpaceRef colorSpaceRef = colorSpace.CGColorSpace; 87 | CGColorRef result = CGColorCreate(colorSpaceRef, components); 88 | 89 | return (CGColorRef)[(id)result autorelease]; 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /RebelTests/NSColor+RBLCGColorAdditionsSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSColor+RBLCGColorAdditionsSpec.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-07-29. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(NSColorRBLCGColorAdditions) 14 | 15 | describe(@"CGColor from NSColor", ^{ 16 | __block NSColor *nsColor; 17 | 18 | beforeEach(^{ 19 | nsColor = nil; 20 | }); 21 | 22 | afterEach(^{ 23 | expect(nsColor).notTo(beNil()); 24 | 25 | CGColorRef rblColor = nsColor.rbl_CGColor; 26 | expect((__bridge id)rblColor).notTo(beNil()); 27 | 28 | if ([NSColor instancesRespondToSelector:@selector(CGColor)]) { 29 | // The result of our method should match that of the 10.8 API. 30 | CGColorRef cgColor = nsColor.CGColor; 31 | expect((__bridge id)cgColor).notTo(beNil()); 32 | 33 | expect(@(CGColorEqualToColor(rblColor, cgColor))).to(beTruthy()); 34 | } 35 | }); 36 | 37 | it(@"should return a CGColor for an predefined NSColor", ^{ 38 | nsColor = [NSColor redColor]; 39 | }); 40 | 41 | it(@"should return a CGColor for an RGB NSColor", ^{ 42 | nsColor = [NSColor colorWithCalibratedRed:0.75 green:0.5 blue:0.25 alpha:0.1]; 43 | }); 44 | }); 45 | 46 | describe(@"NSColor from CGColor", ^{ 47 | __block CGColorRef cgColor; 48 | 49 | beforeEach(^{ 50 | cgColor = NULL; 51 | }); 52 | 53 | afterEach(^{ 54 | expect((__bridge id)cgColor).notTo(beNil()); 55 | 56 | NSColor *rblColor = [NSColor rbl_colorWithCGColor:cgColor]; 57 | expect(rblColor).notTo(beNil()); 58 | 59 | if ([NSColor respondsToSelector:@selector(colorWithCGColor:)]) { 60 | // The result of our method should match that of the 10.8 API. 61 | NSColor *nsColor = [NSColor colorWithCGColor:cgColor]; 62 | expect(rblColor).to(equal(nsColor)); 63 | } 64 | }); 65 | 66 | it(@"should return an NSColor for a constant CGColor", ^{ 67 | cgColor = CGColorGetConstantColor(kCGColorWhite); 68 | }); 69 | 70 | it(@"should return an NSColor for an RGB CGColor", ^{ 71 | cgColor = CGColorCreateGenericRGB(0.75, 0.5, 0.25, 0.1); 72 | }); 73 | 74 | it(@"should return an NSColor for a gray CGColor", ^{ 75 | cgColor = CGColorCreateGenericGray(0.5, 0.75); 76 | }); 77 | }); 78 | 79 | it(@"should return a pattern CGColor", ^{ 80 | NSURL *imageURL = [[NSBundle bundleForClass:self.class] URLForResource:@"_testimage" withExtension:@"jpg"]; 81 | expect(imageURL).notTo(beNil()); 82 | 83 | NSImage *image = [[NSImage alloc] initByReferencingURL:imageURL]; 84 | expect(image).notTo(beNil()); 85 | 86 | NSColor *patternNSColor = [NSColor colorWithPatternImage:image]; 87 | expect(patternNSColor).notTo(beNil()); 88 | 89 | CGColorRef patternCGColor = patternNSColor.rbl_CGColor; 90 | expect((__bridge id)patternCGColor).notTo(beNil()); 91 | expect((__bridge id)CGColorGetPattern(patternCGColor)).notTo(beNil()); 92 | }); 93 | 94 | QuickSpecEnd 95 | -------------------------------------------------------------------------------- /script/README.md: -------------------------------------------------------------------------------- 1 | # objc-build-scripts 2 | 3 | This project is a collection of scripts created with two goals: 4 | 5 | 1. To standardize how Objective-C projects are bootstrapped after cloning 6 | 1. To easily build Objective-C projects on continuous integration servers 7 | 8 | ## Scripts 9 | 10 | Right now, there are two important scripts: [`bootstrap`](#bootstrap) and 11 | [`cibuild`](#cibuild). Both are Bash scripts, to maximize compatibility and 12 | eliminate pesky system configuration issues (like setting up a working Ruby 13 | environment). 14 | 15 | The structure of the scripts on disk is meant to follow that of a typical Ruby 16 | project: 17 | 18 | ``` 19 | script/ 20 | bootstrap 21 | cibuild 22 | ``` 23 | 24 | ### bootstrap 25 | 26 | This script is responsible for bootstrapping (initializing) your project after 27 | it's been checked out. Here, you should install or clone any dependencies that 28 | are required for a working build and development environment. 29 | 30 | By default, the script will verify that [xctool][] is installed, then initialize 31 | and update submodules recursively. If any submodules contain `script/bootstrap`, 32 | that will be run as well. 33 | 34 | To check that other tools are installed, you can set the `REQUIRED_TOOLS` 35 | environment variable before running `script/bootstrap`, or edit it within the 36 | script directly. Note that no installation is performed automatically, though 37 | this can always be added within your specific project. 38 | 39 | ### cibuild 40 | 41 | This script is responsible for building the project, as you would want it built 42 | for continuous integration. This is preferable to putting the logic on the CI 43 | server itself, since it ensures that any changes are versioned along with the 44 | source. 45 | 46 | By default, the script will run [`bootstrap`](#bootstrap), look for any Xcode 47 | workspace or project in the working directory, then build all targets/schemes 48 | (as found by `xcodebuild -list`) using [xctool][]. 49 | 50 | You can also specify the schemes to build by passing them into the script: 51 | 52 | ```sh 53 | script/cibuild ReactiveCocoa-Mac ReactiveCocoa-iOS 54 | ``` 55 | 56 | As with the `bootstrap` script, there are several environment variables that can 57 | be used to customize behavior. They can be set on the command line before 58 | invoking the script, or the defaults changed within the script directly. 59 | 60 | ## Getting Started 61 | 62 | To add the scripts to your project, read the contents of this repository into 63 | a `script` folder: 64 | 65 | ``` 66 | $ git remote add objc-build-scripts https://github.com/jspahrsummers/objc-build-scripts.git 67 | $ git fetch objc-build-scripts 68 | $ git read-tree --prefix=script/ -u objc-build-scripts/master 69 | ``` 70 | 71 | Then commit the changes, to incorporate the scripts into your own repository's 72 | history. You can also freely tweak the scripts for your specific project's 73 | needs. 74 | 75 | To merge in upstream changes later: 76 | 77 | ``` 78 | $ git fetch -p objc-build-scripts 79 | $ git merge --ff --squash -Xsubtree=script objc-build-scripts/master 80 | ``` 81 | 82 | [xctool]: https://github.com/facebook/xctool 83 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | **Copyright (c) 2012 - 2013, GitHub, Inc.** 2 | **All rights reserved.** 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | 10 | --- 11 | 12 | **This project uses portions of code from the Velvet framework.** 13 | **Velvet is copyright (c) 2012, Bitswift, Inc.** 14 | **All rights reserved.** 15 | 16 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 17 | 18 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 19 | * Neither the name of the Bitswift, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 | 23 | --- 24 | 25 | **This project uses portions of code from the TwUI framework.** 26 | **TwUI is copyright 2011 Twitter, Inc.** 27 | 28 | Licensed under the Apache License, Version 2.0 (the "License"); 29 | you may not use this file except in compliance with the License. 30 | You may obtain a copy of the License at 31 | 32 | http://www.apache.org/licenses/LICENSE-2.0 33 | 34 | Unless required by applicable law or agreed to in writing, software 35 | distributed under the License is distributed on an "AS IS" BASIS, 36 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 37 | See the License for the specific language governing permissions and 38 | limitations under the License. 39 | -------------------------------------------------------------------------------- /Rebel/RBLSlidingContainerView.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLSlidingContainerView.h 3 | // Rebel 4 | // 5 | // Created by Alan Rogers on 4/02/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | // Defines the different types of animation used to transition the contentView 12 | // in a RBLSlidingContainerView. 13 | // 14 | // RBLSlidingContainerViewSlideDirectionNone - No animation is used to 15 | // transition the contentView. 16 | // RBLSlidingContainerViewSlideDirectionFromLeft - The containerView is first 17 | // animated to match the bounds 18 | // of the new contentView, then 19 | // the old contentView slides 20 | // away to the right as the new 21 | // contentView slides in from 22 | // the left. 23 | // RBLSlidingContainerViewSlideDirectionFromRight - The containerView is first 24 | // animated to match the bounds 25 | // of the new contentView, then 26 | // the old contentView slides 27 | // away to the left as the new 28 | // contentView slides in from 29 | // the right. 30 | typedef enum : NSUInteger { 31 | RBLSlidingContainerViewSlideDirectionNone = 0, 32 | RBLSlidingContainerViewSlideDirectionFromLeft, 33 | RBLSlidingContainerViewSlideDirectionFromRight 34 | } RBLSlidingContainerViewSlideDirection; 35 | 36 | // A view class that uses Auto Layout to adjust its bounds to fit 37 | // a new contentView. It can conditionally animate this change including a 38 | // CoreAnimation slide transition. During the bounds change, 39 | // the contentView is pinned to the top left. 40 | // 41 | // NOTE: You must use to the provided -setContentView: methods to change the 42 | // subview. Calling any of the normal subview mutating methods on the receiver 43 | // will result in undefined behavior. 44 | // IMPORTANT NOTE: Auto Layout will be enabled for any NSWindow you add this 45 | // view to. 46 | @interface RBLSlidingContainerView : NSView 47 | 48 | // The contentView (single subview) of the receiver. 49 | // 50 | // Setting this property is equivalent to calling 51 | // -setContentView:direction:completion: with animations disabled and 52 | // a nil completion block. 53 | @property (nonatomic, strong) NSView *contentView; 54 | 55 | // Calls -setContentView:direction:completion: with a nil completionBlock. 56 | - (void)setContentView:(NSView *)contentView direction:(RBLSlidingContainerViewSlideDirection)direction; 57 | 58 | // This method will replace the receiver's only subview with contentView. 59 | // 60 | // contentView - The view to be contained in the receiver. 61 | // direction - If not RBLSlidingContainerViewSlideDirectionNone, 62 | // the receiver's bounds will first animate to fit 63 | // the new contentView (pinned to the top left), 64 | // then contentView will slide transition into place according to 65 | // the direction; 66 | // RBLSlidingContainerViewSlideDirectionFromLeft 67 | // or RBLSlidingContainerViewSlideDirectionFromRight. 68 | // completion - An optional block to invoke upon completion of the animation. 69 | // May be nil. 70 | - (void)setContentView:(NSView *)contentView direction:(RBLSlidingContainerViewSlideDirection)direction completion:(void (^)(void))completionBlock; 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /Rebel/RBLHTMLView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLHTMLView.m 3 | // Rebel 4 | // 5 | // Created by Josh Abernathy on 3/13/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLHTMLView.h" 10 | 11 | // These protocols are informal on 10.10, but required on 10.11. 😞 12 | #ifdef MAC_OS_X_VERSION_10_11 13 | @interface RBLHTMLView () 14 | @end 15 | #endif 16 | 17 | @implementation RBLHTMLView 18 | 19 | #pragma mark Lifecycle 20 | 21 | static void CommonInit(RBLHTMLView *self) { 22 | self.drawsBackground = NO; 23 | self.maintainsBackForwardList = NO; 24 | self.mainFrame.frameView.allowsScrolling = NO; 25 | self.policyDelegate = self; 26 | self.UIDelegate = self; 27 | } 28 | 29 | - (id)initWithFrame:(NSRect)frameRect { 30 | self = [super initWithFrame:frameRect]; 31 | if (self == nil) return nil; 32 | 33 | CommonInit(self); 34 | 35 | return self; 36 | } 37 | 38 | - (id)initWithCoder:(NSCoder *)decoder { 39 | self = [super initWithCoder:decoder]; 40 | if (self == nil) return nil; 41 | 42 | CommonInit(self); 43 | 44 | return self; 45 | } 46 | 47 | #pragma mark WebPolicyDelegate 48 | 49 | - (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id)listener { 50 | // WebNavigationTypeOther is when we do our HTML string load. Otherwise, we 51 | // shunt it off to a Real Browser®. 52 | if ([actionInformation[WebActionNavigationTypeKey] integerValue] == WebNavigationTypeOther) { 53 | [listener use]; 54 | } else { 55 | [listener ignore]; 56 | [NSWorkspace.sharedWorkspace openURL:request.URL]; 57 | } 58 | } 59 | 60 | #pragma mark WebUIDelegate 61 | 62 | - (NSArray *)webView:(WebView *)sender contextMenuItemsForElement:(NSDictionary *)element defaultMenuItems:(NSArray *)defaultMenuItems { 63 | NSMutableArray *strippedMenuItems = [[NSMutableArray alloc] initWithArray:defaultMenuItems]; 64 | for (NSMenuItem *item in defaultMenuItems) { 65 | NSUInteger tag = item.tag; 66 | if (tag == WebMenuItemTagReload || tag == WebMenuItemTagGoBack || tag == WebMenuItemTagGoForward || tag == WebMenuItemTagStop || tag == WebMenuItemTagDownloadLinkToDisk) [strippedMenuItems removeObject:item]; 67 | } 68 | 69 | return strippedMenuItems; 70 | } 71 | 72 | - (NSUInteger)webView:(WebView *)webView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo { 73 | return WebDragDestinationActionNone; 74 | } 75 | 76 | #pragma mark HTML 77 | 78 | - (void)setHTML:(NSString *)HTML { 79 | if ([_HTML isEqual:HTML]) return; 80 | 81 | _HTML = [HTML copy]; 82 | 83 | [self reloadConstructedContent]; 84 | } 85 | 86 | - (void)setCSS:(NSString *)CSS { 87 | if ([_CSS isEqual:CSS]) return; 88 | 89 | _CSS = [CSS copy]; 90 | 91 | [self reloadConstructedContent]; 92 | } 93 | 94 | - (void)reloadConstructedContent { 95 | if (self.HTML == nil) return; 96 | 97 | static NSString * const template = @"" 98 | "" 99 | " " 120 | "%@ "; 121 | 122 | NSString *constructedHTML = [NSString stringWithFormat:template, self.CSS, self.HTML]; 123 | [self.mainFrame loadHTMLString:constructedHTML baseURL:nil]; 124 | while (self.isLoading) { 125 | [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]]; 126 | } 127 | } 128 | 129 | @end 130 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/RPDAppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // RPDAppDelegate.m 3 | // RBLPopoverDemo 4 | // 5 | // Created by Matt Diephouse on 2/24/14. 6 | // Copyright (c) 2014 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RPDAppDelegate.h" 10 | 11 | #import "RPDContentViewController.h" 12 | 13 | @interface RPDAppDelegate () 14 | 15 | @property (strong, nonatomic) IBOutlet NSSegmentedControl *preferredEdgeControl; 16 | @property (strong, nonatomic) IBOutlet NSSegmentedControl *behaviorControl; 17 | 18 | @property (strong, nonatomic, readonly) NSPopover *NSPopover; 19 | @property (strong, nonatomic, readonly) RBLPopover *RBLPopover; 20 | 21 | @property (strong, nonatomic) NSNumber *arrowWidth; 22 | @property (strong, nonatomic) NSNumber *arrowHeight; 23 | 24 | @property (strong, nonatomic) NSNumber *anchorPointX; 25 | @property (strong, nonatomic) NSNumber *anchorPointY; 26 | 27 | @end 28 | 29 | @implementation RPDAppDelegate 30 | 31 | #pragma mark - NSApplicationDelegate 32 | 33 | - (instancetype)init { 34 | self = [super init]; 35 | if (self == nil) return nil; 36 | 37 | _NSPopover = [NSPopover new]; 38 | self.NSPopover.contentViewController = [RPDContentViewController new]; 39 | 40 | _RBLPopover = [[RBLPopover alloc] initWithContentViewController:[RPDContentViewController new]]; 41 | self.RBLPopover.canBecomeKey = YES; 42 | 43 | return self; 44 | } 45 | 46 | #pragma mark - Actions 47 | 48 | - (IBAction)showNSPopover:(id)sender { 49 | if (self.NSPopover.shown) { 50 | [self.NSPopover close]; 51 | } else { 52 | NSView *button = sender; 53 | self.NSPopover.behavior = self.behavior; 54 | 55 | NSRectEdge edge = self.preferredEdge; 56 | if (button.isFlipped) { 57 | if (edge == NSMaxYEdge) { 58 | edge = NSMinYEdge; 59 | } else if (edge == NSMinYEdge) { 60 | edge = NSMaxYEdge; 61 | } 62 | } 63 | [self.NSPopover showRelativeToRect:button.bounds ofView:button preferredEdge:edge]; 64 | } 65 | } 66 | 67 | - (IBAction)showRBLPopover:(id)sender { 68 | if (self.RBLPopover.shown) { 69 | [self.RBLPopover close]; 70 | } else { 71 | NSView *button = sender; 72 | self.RBLPopover.behavior = self.behavior; 73 | [self.RBLPopover showRelativeToRect:button.bounds ofView:button preferredEdge:(CGRectEdge)self.preferredEdge]; 74 | } 75 | } 76 | 77 | #pragma mark - Configurable Properties 78 | 79 | - (NSPopoverBehavior)behavior { 80 | NSInteger segment = self.behaviorControl.selectedSegment; 81 | return [self.behaviorControl.cell tagForSegment:segment]; 82 | } 83 | 84 | - (NSRectEdge)preferredEdge { 85 | NSInteger segment = self.preferredEdgeControl.selectedSegment; 86 | return (CGRectEdge)[self.preferredEdgeControl.cell tagForSegment:segment]; 87 | } 88 | 89 | - (NSNumber *)arrowHeight { 90 | return @(self.RBLPopover.backgroundView.arrowSize.height); 91 | } 92 | 93 | - (void)setArrowHeight:(NSNumber *)arrowHeight { 94 | CGSize size = self.RBLPopover.backgroundView.arrowSize; 95 | size.height = arrowHeight.integerValue; 96 | self.RBLPopover.backgroundView.arrowSize = size; 97 | } 98 | 99 | - (NSNumber *)arrowWidth { 100 | return @(self.RBLPopover.backgroundView.arrowSize.width); 101 | } 102 | 103 | - (void)setArrowWidth:(NSNumber *)arrowWidth { 104 | CGSize size = self.RBLPopover.backgroundView.arrowSize; 105 | size.width = arrowWidth.integerValue; 106 | self.RBLPopover.backgroundView.arrowSize = size; 107 | } 108 | 109 | - (NSNumber *)anchorPointX { 110 | return @(self.RBLPopover.anchorPoint.x); 111 | } 112 | 113 | - (void)setAnchorPointX:(NSNumber *)anchorPointX { 114 | self.RBLPopover.anchorPoint = (CGPoint){ .x = anchorPointX.floatValue, .y = self.RBLPopover.anchorPoint.y }; 115 | } 116 | 117 | - (NSNumber *)anchorPointY { 118 | return @(self.RBLPopover.anchorPoint.y); 119 | } 120 | 121 | - (void)setAnchorPointY:(NSNumber *)anchorPointY { 122 | self.RBLPopover.anchorPoint = (CGPoint){ .x = self.RBLPopover.anchorPoint.x, .y = anchorPointY.floatValue }; 123 | } 124 | 125 | @end 126 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export SCRIPT_DIR=$(dirname "$0") 4 | 5 | ## 6 | ## Configuration Variables 7 | ## 8 | 9 | SCHEMES="$@" 10 | 11 | config () 12 | { 13 | # The workspace to build. 14 | # 15 | # If not set and no workspace is found, the -workspace flag will not be passed 16 | # to `xctool`. 17 | # 18 | # Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will 19 | # take precedence. 20 | : ${XCWORKSPACE=$(find_pattern "*.xcworkspace")} 21 | 22 | # The project to build. 23 | # 24 | # If not set and no project is found, the -project flag will not be passed 25 | # to `xctool`. 26 | # 27 | # Only one of `XCWORKSPACE` and `XCODEPROJ` needs to be set. The former will 28 | # take precedence. 29 | : ${XCODEPROJ=$(find_pattern "*.xcodeproj")} 30 | 31 | # A bootstrap script to run before building. 32 | # 33 | # If this file does not exist, it is not considered an error. 34 | : ${BOOTSTRAP="$SCRIPT_DIR/bootstrap"} 35 | 36 | # Extra options to pass to xctool. 37 | : ${XCTOOL_OPTIONS="RUN_CLANG_STATIC_ANALYZER=NO"} 38 | 39 | # A whitespace-separated list of default schemes to build. 40 | # 41 | # Individual names can be quoted to avoid word splitting. 42 | : ${SCHEMES:=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk -f "$SCRIPT_DIR/schemes.awk")} 43 | 44 | export XCWORKSPACE 45 | export XCODEPROJ 46 | export BOOTSTRAP 47 | export XCTOOL_OPTIONS 48 | export SCHEMES 49 | } 50 | 51 | ## 52 | ## Build Process 53 | ## 54 | 55 | main () 56 | { 57 | config 58 | 59 | if [ -f "$BOOTSTRAP" ] 60 | then 61 | echo "*** Bootstrapping..." 62 | "$BOOTSTRAP" || exit $? 63 | fi 64 | 65 | echo "*** The following schemes will be built:" 66 | echo "$SCHEMES" | xargs -n 1 echo " " 67 | echo 68 | 69 | echo "$SCHEMES" | xargs -n 1 | ( 70 | local status=0 71 | 72 | while read scheme 73 | do 74 | build_scheme "$scheme" || status=1 75 | done 76 | 77 | exit $status 78 | ) 79 | } 80 | 81 | find_pattern () 82 | { 83 | ls -d $1 2>/dev/null | head -n 1 84 | } 85 | 86 | run_xctool () 87 | { 88 | if [ -n "$XCWORKSPACE" ] 89 | then 90 | xctool -workspace "$XCWORKSPACE" $XCTOOL_OPTIONS "$@" 2>&1 91 | elif [ -n "$XCODEPROJ" ] 92 | then 93 | xctool -project "$XCODEPROJ" $XCTOOL_OPTIONS "$@" 2>&1 94 | else 95 | echo "*** No workspace or project file found." 96 | exit 1 97 | fi 98 | } 99 | 100 | parse_build () 101 | { 102 | awk -f "$SCRIPT_DIR/xctool.awk" 2>&1 >/dev/null 103 | } 104 | 105 | build_scheme () 106 | { 107 | local scheme=$1 108 | 109 | echo "*** Cleaning $scheme..." 110 | run_xctool -scheme "$scheme" clean >/dev/null || exit $? 111 | 112 | echo "*** Building and testing $scheme..." 113 | echo 114 | 115 | local sdkflag= 116 | local action=test 117 | 118 | # Determine whether we can run unit tests for this target. 119 | run_xctool -scheme "$scheme" run-tests | parse_build 120 | 121 | local awkstatus=$? 122 | 123 | if [ "$awkstatus" -eq "1" ] 124 | then 125 | # SDK not found, try for iphonesimulator. 126 | sdkflag="-sdk iphonesimulator" 127 | 128 | # Determine whether the unit tests will run with iphonesimulator 129 | run_xctool $sdkflag -scheme "$scheme" run-tests | parse_build 130 | 131 | awkstatus=$? 132 | 133 | if [ "$awkstatus" -ne "0" ] 134 | then 135 | # Unit tests will not run on iphonesimulator. 136 | sdkflag="" 137 | fi 138 | fi 139 | 140 | if [ "$awkstatus" -ne "0" ] 141 | then 142 | # Unit tests aren't supported. 143 | action=build 144 | fi 145 | 146 | run_xctool $sdkflag -scheme "$scheme" $action 147 | } 148 | 149 | export -f build_scheme 150 | export -f run_xctool 151 | export -f parse_build 152 | 153 | main 154 | -------------------------------------------------------------------------------- /RebelTests/RBLResizableImageSpec.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLResizableImageSpec.m 3 | // Rebel 4 | // 5 | // Created by Alan Rogers on 16/04/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import 12 | 13 | QuickSpecBegin(RBLResizableImageSpec) 14 | 15 | __block RBLResizableImage *testImage = nil; 16 | __block void(^expectBlueBorder)(NSBitmapImageRep* imageRep, NSSize size) = nil; 17 | 18 | beforeEach(^{ 19 | NSURL *testImageURL = [[NSBundle bundleForClass:self.class] URLForResource:@"_testimage" withExtension:@"tiff"]; 20 | 21 | expect(testImageURL).notTo(beNil()); 22 | 23 | testImage = [[RBLResizableImage alloc] initByReferencingURL:testImageURL]; 24 | 25 | expect(testImage).notTo(beNil()); 26 | 27 | expectBlueBorder = ^(NSBitmapImageRep *imageRep, NSSize size) { 28 | NSUInteger topLeft[4], bottomRight[4]; 29 | 30 | // confirm we have the borders 31 | [imageRep getPixel:&topLeft[0] atX:0 y:0]; 32 | 33 | NSUInteger red = topLeft[0]; 34 | NSUInteger green = topLeft[1]; 35 | NSUInteger blue = topLeft[2]; 36 | NSUInteger alpha = topLeft[3]; 37 | 38 | expect(@(red)).to(equal(@0)); 39 | expect(@(green)).to(equal(@0)); 40 | expect(@(blue)).to(equal(@255)); 41 | expect(@(alpha)).to(equal(@255)); 42 | 43 | [imageRep getPixel:&bottomRight[0] atX:(NSUInteger)(size.width - 1) y:(NSUInteger)(size.height - 1)]; 44 | 45 | red = bottomRight[0]; 46 | green = bottomRight[1]; 47 | blue = bottomRight[2]; 48 | alpha = bottomRight[3]; 49 | 50 | expect(@(red)).to(equal(@0)); 51 | expect(@(green)).to(equal(@0)); 52 | expect(@(blue)).to(equal(@255)); 53 | expect(@(alpha)).to(equal(@255)); 54 | }; 55 | }); 56 | 57 | it(@"should use @1x asset in @1x context", ^{ 58 | CGSize targetSize = { .width = 50, .height = 26 }; 59 | 60 | NSBitmapImageRep *bitmapImageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:(NSInteger)targetSize.width pixelsHigh:(NSInteger)targetSize.height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bitmapFormat:0 bytesPerRow:0 bitsPerPixel:0]; 61 | 62 | NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapImageRep]; 63 | 64 | [NSGraphicsContext setCurrentContext:context]; 65 | 66 | [testImage drawInRect:CGRectZero fromRect:NSZeroRect operation:NSCompositeCopy fraction:1. respectFlipped:YES hints:nil]; 67 | 68 | [context flushGraphics]; 69 | 70 | NSUInteger testPixel[4]; 71 | 72 | [bitmapImageRep getPixel:&testPixel[0] atX:24 y:10]; 73 | 74 | NSUInteger red = testPixel[0]; 75 | NSUInteger green = testPixel[1]; 76 | NSUInteger blue = testPixel[2]; 77 | NSUInteger alpha = testPixel[3]; 78 | 79 | // Should be a red pixel here if we used the @1x asset 80 | expect(@(red)).to(equal(@255)); 81 | expect(@(green)).to(equal(@0)); 82 | expect(@(blue)).to(equal(@0)); 83 | expect(@(alpha)).to(equal(@255)); 84 | 85 | expectBlueBorder(bitmapImageRep, targetSize); 86 | }); 87 | 88 | it(@"should use @2x asset in @2x context", ^{ 89 | CGSize targetSize = { .width = 100, .height = 52 }; 90 | 91 | NSBitmapImageRep *bitmapImageRep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:(NSInteger)targetSize.width pixelsHigh:(NSInteger)targetSize.height bitsPerSample:8 samplesPerPixel:4 hasAlpha:YES isPlanar:NO colorSpaceName:NSDeviceRGBColorSpace bitmapFormat:0 bytesPerRow:0 bitsPerPixel:0]; 92 | 93 | NSGraphicsContext *context = [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapImageRep]; 94 | 95 | [NSGraphicsContext setCurrentContext:context]; 96 | 97 | [testImage drawInRect:NSZeroRect fromRect:NSZeroRect operation:NSCompositeCopy fraction:1. respectFlipped:YES hints:nil]; 98 | 99 | [context flushGraphics]; 100 | 101 | NSUInteger testPixel[4]; 102 | 103 | [bitmapImageRep getPixel:&testPixel[0] atX:24 y:10]; 104 | 105 | NSUInteger red = testPixel[0]; 106 | NSUInteger green = testPixel[1]; 107 | NSUInteger blue = testPixel[2]; 108 | NSUInteger alpha = testPixel[3]; 109 | 110 | // Should be a green pixel here if we used the @1x asset 111 | expect(@(red)).to(equal(@0)); 112 | expect(@(green)).to(equal(@255)); 113 | expect(@(blue)).to(equal(@0)); 114 | expect(@(alpha)).to(equal(@255)); 115 | 116 | expectBlueBorder(bitmapImageRep, targetSize); 117 | }); 118 | 119 | QuickSpecEnd 120 | -------------------------------------------------------------------------------- /Rebel.xcodeproj/xcshareddata/xcschemes/Rebel.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 79 | 80 | 86 | 87 | 88 | 89 | 90 | 91 | 97 | 98 | 104 | 105 | 106 | 107 | 109 | 110 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /Rebel/RBLExpandingContainerView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLExpandingContainerView.m 3 | // Rebel 4 | // 5 | // Created by Alan Rogers on 4/02/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLExpandingContainerView.h" 10 | #import "NSView+RBLAnimationAdditions.h" 11 | 12 | static const NSTimeInterval RBLExpandingContainerViewAnimationDuration = 0.2; 13 | 14 | @interface RBLExpandingContainerView () 15 | 16 | // A temporary constraint used to change the height of the receiver. 17 | @property (nonatomic, strong) NSLayoutConstraint *heightConstraint; 18 | 19 | // A temporary constraint used to change the width of the receiver. 20 | @property (nonatomic, strong) NSLayoutConstraint *widthConstraint; 21 | 22 | // Constraints that keep the contentView horizontally contained 23 | // within the receiver. 24 | // 25 | // The leading constraint to the superview is always required. 26 | // The trailing constraint to the superview is not required during animation, 27 | // but required when animation is complete. This has the effect of pinning 28 | // the contentView to the leading edge of the receiver during the bounds change 29 | // animation. 30 | @property (nonatomic, strong) NSArray *horizontalContainerConstraints; 31 | 32 | // Constraints that keep the contentView horizontally contained 33 | // within the receiver. 34 | // 35 | // The top constraint to the superview is always required. 36 | // The bottom constraint to the superview is not required during animation, 37 | // but required when animation is complete. This has the effect of pinning 38 | // the contentView to the top edge of the receiver during the bounds change 39 | // animation. 40 | @property (nonatomic, strong) NSArray *verticalContainerConstraints; 41 | 42 | @end 43 | 44 | @implementation RBLExpandingContainerView 45 | 46 | #pragma mark NSView 47 | 48 | + (BOOL)requiresConstraintBasedLayout { 49 | return YES; 50 | } 51 | 52 | #pragma mark contentView 53 | 54 | - (void)setContentView:(NSView *)contentView { 55 | [self setContentView:contentView animated:NO]; 56 | } 57 | 58 | - (void)setContentView:(NSView *)contentView animated:(BOOL)animated { 59 | [self setContentView:contentView animated:animated completion:nil]; 60 | } 61 | 62 | - (void)setContentView:(NSView *)contentView animated:(BOOL)animated completion:(void (^)(void))completionBlock { 63 | CGSize startingSize = self.contentView.frame.size; 64 | 65 | // Remove the existing contentView 66 | [self.contentView removeFromSuperview]; 67 | 68 | // We want our new containerView fully laid out before we continue (it's 69 | // usually lazy) 70 | [contentView layoutSubtreeIfNeeded]; 71 | 72 | [self addSubview:contentView]; 73 | _contentView = contentView; 74 | 75 | void (^completeExpansion)() = ^{ 76 | if (animated) self.wantsLayer = NO; 77 | 78 | // Just remove all constraints as we only want the ones below 79 | [self removeConstraints:self.constraints]; 80 | 81 | // Add required fitting constraints so that the contentView fits 82 | // snugly in the containerView 83 | self.verticalContainerConstraints = [NSLayoutConstraint 84 | constraintsWithVisualFormat:@"V:|[contentView]|" 85 | options:0 86 | metrics:nil 87 | views:@{ @"contentView": self.contentView }]; 88 | self.horizontalContainerConstraints = [NSLayoutConstraint 89 | constraintsWithVisualFormat:@"H:|[contentView]|" 90 | options:0 91 | metrics:nil 92 | views:@{ @"contentView": self.contentView }]; 93 | 94 | [self addConstraints:self.verticalContainerConstraints]; 95 | [self addConstraints:self.horizontalContainerConstraints]; 96 | 97 | if (completionBlock != nil) completionBlock(); 98 | }; 99 | 100 | if (!animated) { 101 | completeExpansion(); 102 | return; 103 | } 104 | 105 | self.wantsLayer = YES; 106 | [self addAnimationConstraintsWithStartingSize:startingSize]; 107 | [NSView rbl_animateWithDuration:RBLExpandingContainerViewAnimationDuration animations:^{ 108 | [self.heightConstraint.animator setConstant:self.contentView.frame.size.height]; 109 | [self.widthConstraint.animator setConstant:self.contentView.frame.size.width]; 110 | } completion:completeExpansion]; 111 | } 112 | 113 | #pragma mark Private 114 | 115 | - (void)addAnimationConstraintsWithStartingSize:(CGSize)size { 116 | [self removeConstraints:self.constraints]; 117 | 118 | self.heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:0 constant:size.height]; 119 | 120 | self.widthConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeWidth multiplier:0 constant:size.width]; 121 | 122 | [self addConstraints:@[ self.heightConstraint, self.widthConstraint ]]; 123 | 124 | // We use NSLayoutPriorityDefaultHigh so that our contentView will sit at 125 | // the top left while the containerView changes its bounds around it during 126 | // the animation. 127 | self.verticalContainerConstraints = [NSLayoutConstraint 128 | constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|[contentView]-(0@%f)-|", NSLayoutPriorityDefaultHigh] 129 | options:0 130 | metrics:0 131 | views:@{ @"contentView": self.contentView }]; 132 | self.horizontalContainerConstraints = [NSLayoutConstraint 133 | constraintsWithVisualFormat:[NSString stringWithFormat:@"H:|[contentView]-(0@%f)-|", NSLayoutPriorityDefaultHigh] 134 | options:0 135 | metrics:0 136 | views:@{ @"contentView": self.contentView }]; 137 | 138 | [self addConstraints:self.verticalContainerConstraints]; 139 | [self addConstraints:self.horizontalContainerConstraints]; 140 | } 141 | 142 | @end 143 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo/Base.lproj/RPDContentViewController.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /Rebel/RBLSlidingContainerView.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLSlidingContainerView 3 | // Rebel 4 | // 5 | // Created by Alan Rogers on 4/02/13. 6 | // Copyright (c) 2013 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLSlidingContainerView.h" 10 | #import "NSView+RBLAnimationAdditions.h" 11 | #import 12 | 13 | static const NSTimeInterval RBLTransitioningContainerViewResizeAnimationDuration = 0.2; 14 | static const NSTimeInterval RBLTransitioningContainerViewSlideAnimationDuration = 0.3; 15 | 16 | @interface RBLSlidingContainerView () 17 | 18 | // A temporary constraint used to change the height of the receiver. 19 | @property (nonatomic, strong) NSLayoutConstraint *heightConstraint; 20 | 21 | // A temporary constraint used to change the height of the receiver. 22 | @property (nonatomic, strong) NSLayoutConstraint *widthConstraint; 23 | 24 | // Constraints that keep the contentView horizontally contained 25 | // within the receiver. 26 | // 27 | // The leading constraint to the superview is always required. 28 | // The trailing constraint to the superview is not required during animation, 29 | // but required when animation is complete. This has the effect of pinning 30 | // the contentView to the leading edge of the receiver during the bounds change 31 | // animation. 32 | @property (nonatomic, strong) NSArray *horizontalContainerConstraints; 33 | 34 | // Constraints that keep the contentView horizontally contained 35 | // within the receiver. 36 | // 37 | // The top constraint to the superview is always required. 38 | // The bottom constraint to the superview is not required during animation, 39 | // but required when animation is complete. This has the effect of pinning 40 | // the contentView to the top edge of the receiver during the bounds change 41 | // animation. 42 | @property (nonatomic, strong) NSArray *verticalContainerConstraints; 43 | 44 | @end 45 | 46 | @implementation RBLSlidingContainerView 47 | 48 | #pragma mark NSView 49 | 50 | + (BOOL)requiresConstraintBasedLayout { 51 | return YES; 52 | } 53 | 54 | #pragma mark contentView 55 | 56 | - (void)setContentView:(NSView *)contentView { 57 | [self setContentView:contentView direction:RBLSlidingContainerViewSlideDirectionNone completion:nil]; 58 | } 59 | 60 | - (void)setContentView:(NSView *)contentView direction:(RBLSlidingContainerViewSlideDirection)direction { 61 | [self setContentView:contentView direction:direction completion:nil]; 62 | } 63 | 64 | - (void)setContentView:(NSView *)contentView direction:(RBLSlidingContainerViewSlideDirection)direction completion:(void (^)(void))completionBlock { 65 | BOOL animated = (direction != RBLSlidingContainerViewSlideDirectionNone); 66 | 67 | // We want our new containerView fully laid out before we continue (it's 68 | // usually lazy) 69 | [contentView layoutSubtreeIfNeeded]; 70 | 71 | void(^replaceContentView)(void) = ^{ 72 | [self.contentView.rbl_animator removeFromSuperview]; 73 | [self.rbl_animator addSubview:contentView]; 74 | _contentView = contentView; 75 | }; 76 | void (^completeTransition)(void) = ^{ 77 | if (animated) { 78 | self.wantsLayer = NO; 79 | } 80 | [self removeConstraints:self.constraints]; 81 | 82 | // Add required fitting constraints so that the contentView fits 83 | // snugly in the containerView 84 | self.verticalContainerConstraints = [NSLayoutConstraint 85 | constraintsWithVisualFormat:@"V:|[contentView]|" 86 | options:0 87 | metrics:0 88 | views:@{ @"contentView": self.contentView }]; 89 | self.horizontalContainerConstraints = [NSLayoutConstraint 90 | constraintsWithVisualFormat:@"H:|[contentView]|" 91 | options:0 92 | metrics:0 93 | views:@{ @"contentView": self.contentView }]; 94 | 95 | [self addConstraints:self.verticalContainerConstraints]; 96 | [self addConstraints:self.horizontalContainerConstraints]; 97 | 98 | self.animations = @{}; 99 | 100 | if (completionBlock != nil) completionBlock(); 101 | }; 102 | 103 | if (!animated) { 104 | replaceContentView(); 105 | completeTransition(); 106 | return; 107 | } 108 | 109 | self.wantsLayer = YES; 110 | [self addAnimationConstraintsWithStartingSize:self.contentView.frame.size]; 111 | 112 | [NSView rbl_animateWithDuration:RBLTransitioningContainerViewResizeAnimationDuration animations:^{ 113 | [self.heightConstraint.animator setConstant:contentView.frame.size.height]; 114 | [self.widthConstraint.animator setConstant:contentView.frame.size.width]; 115 | } 116 | completion:^{ 117 | CATransition *transition = [CATransition animation]; 118 | transition.type = kCATransitionPush; 119 | transition.subtype = (direction == RBLSlidingContainerViewSlideDirectionFromLeft ? kCATransitionFromLeft : kCATransitionFromRight); 120 | transition.duration = RBLTransitioningContainerViewSlideAnimationDuration; 121 | transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 122 | 123 | self.animations = @{ @"subviews" : transition }; 124 | [NSView rbl_animateWithDuration:RBLTransitioningContainerViewSlideAnimationDuration animations:replaceContentView completion:completeTransition]; 125 | }]; 126 | } 127 | 128 | #pragma mark Private 129 | 130 | - (void)addAnimationConstraintsWithStartingSize:(CGSize)size { 131 | [self removeConstraints:self.constraints]; 132 | self.heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeHeight multiplier:0 constant:size.height]; 133 | 134 | self.widthConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeWidth multiplier:0 constant:size.width]; 135 | 136 | [self addConstraints:@[ self.heightConstraint, self.widthConstraint ]]; 137 | 138 | // We use NSLayoutPriorityDefaultHigh because so that our contentView 139 | // will sit at the top left while the containerView changes its bounds 140 | // around it during the animation. 141 | self.verticalContainerConstraints = [NSLayoutConstraint 142 | constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|[contentView]-(0@%f)-|", NSLayoutPriorityDefaultHigh] 143 | options:0 144 | metrics:0 145 | views:@{ @"contentView": self.contentView }]; 146 | self.horizontalContainerConstraints = [NSLayoutConstraint 147 | constraintsWithVisualFormat:[NSString stringWithFormat:@"H:|[contentView]-(0@%f)-|", NSLayoutPriorityDefaultHigh] 148 | options:0 149 | metrics:0 150 | views:@{ @"contentView": self.contentView }]; 151 | 152 | [self addConstraints:self.verticalContainerConstraints]; 153 | [self addConstraints:self.horizontalContainerConstraints]; 154 | } 155 | 156 | @end 157 | -------------------------------------------------------------------------------- /Rebel/RBLResizableImage.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLResizableImage.m 3 | // Rebel 4 | // 5 | // Created by Justin Spahr-Summers on 2012-07-24. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | // Portions copyright (c) 2011 Twitter. All rights reserved. 9 | // See the LICENSE file for more information. 10 | // 11 | 12 | #import "RBLResizableImage.h" 13 | 14 | @implementation RBLResizableImage 15 | 16 | #pragma mark Drawing 17 | 18 | - (void)drawInRect:(NSRect)dstRect fromRect:(NSRect)srcRect operation:(NSCompositingOperation)op fraction:(CGFloat)alpha { 19 | [self drawInRect:dstRect fromRect:srcRect operation:op fraction:alpha respectFlipped:YES hints:nil]; 20 | } 21 | 22 | - (void)drawInRect:(NSRect)dstRect fromRect:(NSRect)srcRect operation:(NSCompositingOperation)op fraction:(CGFloat)alpha respectFlipped:(BOOL)respectFlipped hints:(NSDictionary *)hints { 23 | if (NSIsEmptyRect(dstRect)) { 24 | CGContextRef context = NSGraphicsContext.currentContext.graphicsPort; 25 | dstRect = CGContextGetClipBoundingBox(context); 26 | } 27 | CGImageRef image = [self CGImageForProposedRect:&dstRect context:NSGraphicsContext.currentContext hints:hints]; 28 | NSAssert(image != NULL, @"Could not get CGImage of %@ for resizing", self); 29 | 30 | // Calculate scale factors from the pixel-independent representation to the 31 | // specific one we're using for this context. 32 | CGFloat widthScale = CGImageGetWidth(image) / self.size.width; 33 | CGFloat heightScale = CGImageGetHeight(image) / self.size.height; 34 | 35 | NSEdgeInsets insets = self.rbl_capInsets; 36 | 37 | // TODO: Cache the nine-part images for this common case of wanting to draw 38 | // the whole source image. 39 | if (CGRectIsEmpty(srcRect)) { 40 | // Match the image creation that occurs in the 'else' clause. 41 | CGImageRetain(image); 42 | } else { 43 | CGRect scaledRect = CGRectMake(srcRect.origin.x * widthScale, srcRect.origin.y * heightScale, srcRect.size.width * widthScale, srcRect.size.height * heightScale); 44 | image = CGImageCreateWithImageInRect(image, scaledRect); 45 | if (image == NULL) return; 46 | 47 | // Reduce insets to account for taking only part of the original image. 48 | insets.left = fmax(0, insets.left - CGRectGetMinX(srcRect)); 49 | insets.bottom = fmax(0, insets.bottom - CGRectGetMinY(srcRect)); 50 | 51 | CGFloat srcRightInset = self.size.width - CGRectGetMaxX(srcRect); 52 | insets.right = fmax(0, insets.right - srcRightInset); 53 | 54 | CGFloat srcTopInset = self.size.height - CGRectGetMaxY(srcRect); 55 | insets.top = fmax(0, insets.top - srcTopInset); 56 | } 57 | 58 | NSImage *topLeft = nil, *topEdge = nil, *topRight = nil; 59 | NSImage *leftEdge = nil, *center = nil, *rightEdge = nil; 60 | NSImage *bottomLeft = nil, *bottomEdge = nil, *bottomRight = nil; 61 | 62 | // Length of sides that run vertically. 63 | CGFloat verticalEdgeLength = fmax(0, self.size.height - insets.top - insets.bottom); 64 | 65 | // Length of sides that run horizontally. 66 | CGFloat horizontalEdgeLength = fmax(0, self.size.width - insets.left - insets.right); 67 | 68 | NSImage *(^imageWithRect)(CGRect) = ^ id (CGRect rect) { 69 | CGRect scaledRect = CGRectMake(rect.origin.x * widthScale, rect.origin.y * heightScale, rect.size.width * widthScale, rect.size.height * heightScale); 70 | CGImageRef part = CGImageCreateWithImageInRect(image, scaledRect); 71 | if (part == NULL) return nil; 72 | 73 | NSImage *image = [[NSImage alloc] initWithCGImage:part size:rect.size]; 74 | CGImageRelease(part); 75 | 76 | return image; 77 | }; 78 | 79 | if (verticalEdgeLength > 0) { 80 | if (insets.left > 0) { 81 | CGRect partRect = CGRectMake(0, insets.bottom, insets.left, verticalEdgeLength); 82 | leftEdge = imageWithRect(partRect); 83 | } 84 | 85 | if (insets.right > 0) { 86 | CGRect partRect = CGRectMake(self.size.width - insets.right, insets.bottom, insets.right, verticalEdgeLength); 87 | rightEdge = imageWithRect(partRect); 88 | } 89 | } 90 | 91 | if (horizontalEdgeLength > 0) { 92 | if (insets.bottom > 0) { 93 | CGRect partRect = CGRectMake(insets.left, 0, horizontalEdgeLength, insets.bottom); 94 | bottomEdge = imageWithRect(partRect); 95 | } 96 | 97 | if (insets.top > 0) { 98 | CGRect partRect = CGRectMake(insets.left, self.size.height - insets.top, horizontalEdgeLength, insets.top); 99 | topEdge = imageWithRect(partRect); 100 | } 101 | } 102 | 103 | if (insets.left > 0 && insets.top > 0) { 104 | CGRect partRect = CGRectMake(0, self.size.height - insets.top, insets.left, insets.top); 105 | topLeft = imageWithRect(partRect); 106 | } 107 | 108 | if (insets.left > 0 && insets.bottom > 0) { 109 | CGRect partRect = CGRectMake(0, 0, insets.left, insets.bottom); 110 | bottomLeft = imageWithRect(partRect); 111 | } 112 | 113 | if (insets.right > 0 && insets.top > 0) { 114 | CGRect partRect = CGRectMake(self.size.width - insets.right, self.size.height - insets.top, insets.right, insets.top); 115 | topRight = imageWithRect(partRect); 116 | } 117 | 118 | if (insets.right > 0 && insets.bottom > 0) { 119 | CGRect partRect = CGRectMake(self.size.width - insets.right, 0, insets.right, insets.bottom); 120 | bottomRight = imageWithRect(partRect); 121 | } 122 | 123 | CGRect centerRect = CGRectMake(insets.left, insets.bottom, horizontalEdgeLength, verticalEdgeLength); 124 | if (centerRect.size.width > 0 && centerRect.size.height > 0) { 125 | center = imageWithRect(centerRect); 126 | } 127 | 128 | CGImageRelease(image); 129 | 130 | BOOL flipped = NO; 131 | if (respectFlipped) { 132 | flipped = [NSGraphicsContext.currentContext isFlipped]; 133 | } 134 | 135 | if (topLeft != nil || bottomRight != nil) { 136 | NSDrawNinePartImage(dstRect, bottomLeft, bottomEdge, bottomRight, leftEdge, center, rightEdge, topLeft, topEdge, topRight, op, alpha, flipped); 137 | } else if (leftEdge != nil) { 138 | // Horizontal three-part image. 139 | NSDrawThreePartImage(dstRect, leftEdge, center, rightEdge, NO, op, alpha, flipped); 140 | } else { 141 | // Vertical three-part image. 142 | NSDrawThreePartImage(dstRect, (flipped ? bottomEdge : topEdge), center, (flipped ? topEdge : bottomEdge), YES, op, alpha, flipped); 143 | } 144 | } 145 | 146 | #pragma mark NSCopying 147 | 148 | - (id)copyWithZone:(NSZone *)zone { 149 | RBLResizableImage *image = [super copyWithZone:zone]; 150 | image.rbl_capInsets = self.rbl_capInsets; 151 | return image; 152 | } 153 | 154 | #pragma mark NSCoding 155 | 156 | - (id)initWithCoder:(NSCoder *)coder { 157 | self = [super initWithCoder:coder]; 158 | if (self == nil) return nil; 159 | 160 | self.rbl_capInsets = NSEdgeInsetsMake( 161 | [coder decodeDoubleForKey:@"capInsetTop"], 162 | [coder decodeDoubleForKey:@"capInsetLeft"], 163 | [coder decodeDoubleForKey:@"capInsetBottom"], 164 | [coder decodeDoubleForKey:@"capInsetRight"] 165 | ); 166 | 167 | return self; 168 | } 169 | 170 | - (void)encodeWithCoder:(NSCoder *)coder { 171 | [super encodeWithCoder:coder]; 172 | 173 | [coder encodeDouble:self.rbl_capInsets.top forKey:@"capInsetTop"]; 174 | [coder encodeDouble:self.rbl_capInsets.left forKey:@"capInsetLeft"]; 175 | [coder encodeDouble:self.rbl_capInsets.bottom forKey:@"capInsetBottom"]; 176 | [coder encodeDouble:self.rbl_capInsets.right forKey:@"capInsetRight"]; 177 | } 178 | 179 | #pragma mark NSObject 180 | 181 | - (BOOL)isEqual:(RBLResizableImage *)image { 182 | if (self == image) return YES; 183 | if (![image isKindOfClass:RBLResizableImage.class]) return NO; 184 | if (![super isEqual:image]) return NO; 185 | 186 | NSEdgeInsets a = self.rbl_capInsets; 187 | NSEdgeInsets b = image.rbl_capInsets; 188 | 189 | if (fabs(a.left - b.left) > 0.1) return NO; 190 | if (fabs(a.top - b.top) > 0.1) return NO; 191 | if (fabs(a.right - b.right) > 0.1) return NO; 192 | if (fabs(a.bottom - b.bottom) > 0.1) return NO; 193 | 194 | return YES; 195 | } 196 | 197 | - (NSString *)description { 198 | NSEdgeInsets insets = self.rbl_capInsets; 199 | return [NSString stringWithFormat:@"<%@: %p>{ size = %@, capInsets = (%f, %f, %f, %f) }", self.class, self, NSStringFromSize(self.size), insets.top, insets.left, insets.bottom, insets.right]; 200 | } 201 | 202 | @end 203 | -------------------------------------------------------------------------------- /Rebel/RBLPopover.h: -------------------------------------------------------------------------------- 1 | // 2 | // RBLPopover.h 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 13/09/2012. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class RBLPopover; 12 | @class RBLPopoverBackgroundView; 13 | 14 | // Defines the different types of behavior of a RBLPopover. 15 | // 16 | // RBLPopoverBehaviorApplicationDefined - The application decides when the 17 | // popover should open and close, by 18 | // doing so manually. 19 | // RBLPopoverBehaviorTransient - If there is a mouse down anywhere 20 | // other than in the popover the popover 21 | // is closed. It is also closed if the 22 | // app or parent window lose focus or 23 | // `esc` is pressed. 24 | // RBLPopoverBehaviorSemiTransient - Closes the popover only if there is a 25 | // mouse up within the popover's parent 26 | // window or `esc` is pressed. 27 | typedef enum : NSUInteger { 28 | RBLPopoverBehaviorApplicationDefined = 0, 29 | RBLPopoverBehaviorTransient = 1, 30 | RBLPopoverBehaviorSemiTransient = 2 31 | } RBLPopoverBehavior; 32 | 33 | typedef void (^RBLPopoverDelegateBlock)(RBLPopover *popover); 34 | 35 | // A popover. 36 | // This aims to replicate the API of `NSPopover`, within reason, whilst offering 37 | // more flexibility when it comes to customising of its appearance. 38 | // 39 | // One notable difference in behaviour when compared to `NSPopover` is memory 40 | // management. `RBLPopover` does not retain itself and when it deallocates it 41 | // will be removed from screen. Therefore a reference must be kept to the popover 42 | // (or it should be otherwise retained) for as long as it should appear on 43 | // screen. 44 | // 45 | // A note on layers: by default the clipping method which the popover uses to 46 | // clip its subviews to its outline does _not_ support any layer backed or 47 | // hosting views. This can be worked around by adding mask layers to any layers 48 | // you add to the popover or its subviews. 49 | @interface RBLPopover : NSResponder 50 | 51 | // The view controller providing the view displayed within the popover. 52 | @property (nonatomic, strong) NSViewController *contentViewController; 53 | 54 | // The popover's background view. You may set your own view, which is useful for 55 | // customizing the appearance of the popover. 56 | @property (nonatomic, strong, readonly) RBLPopoverBackgroundView *backgroundView; 57 | 58 | // The size that, when displayed, the popover's content should be. 59 | // Passing `CGSizeZero` uses the size of the `contentViewController`'s view. 60 | @property (nonatomic) CGSize contentSize; 61 | 62 | /// The anchor point of the popover in unit coordinate space. 63 | /// 64 | /// This specifies the alignment of the popover to the origin view along the 65 | /// secondary axis. If the popover is on the Y edge, this will determine the X 66 | /// position of the popover; it it's on the X edge, it will determine the Y 67 | /// position. 68 | /// 69 | /// A 0 will align the left or bottom edges of the popover and the origin view. 70 | /// A 1 will align the right or top edges. 71 | /// 72 | /// The default value is (0.5, 0.5), indicating the center of the popover. 73 | @property (nonatomic, assign) CGPoint anchorPoint; 74 | 75 | // Whether the next open/close of the popover should be animated. 76 | // Note that this property is checked just before the animation is performed. 77 | // Therefore it is possible to animate the showing of the popover but hide the 78 | // closing and vice versa. 79 | @property (nonatomic) BOOL animates; 80 | 81 | // How the popover should respond to user events, in regard to automatically 82 | // closing the popover. 83 | // See the definition of `RBLPopoverBehavior` for more 84 | // information. 85 | @property (nonatomic) RBLPopoverBehavior behavior; 86 | 87 | // Whether the popover is currently visible. 88 | @property (nonatomic, readonly, getter = isShown) BOOL shown; 89 | 90 | // Called before the popover is closed. 91 | @property (nonatomic, copy) RBLPopoverDelegateBlock willCloseBlock; 92 | 93 | // Called after the close has completed. 94 | // Note that if the close is animated this block will be called _after_ the 95 | // animation has successfully completed. 96 | @property (nonatomic, copy) RBLPopoverDelegateBlock didCloseBlock; 97 | 98 | // Called before the popover is opened. 99 | @property (nonatomic, copy) RBLPopoverDelegateBlock willShowBlock; 100 | 101 | // Called after the block has opened. 102 | // Note that if the open is animated this block will be called _after_ the 103 | // animation has successfully completed. 104 | @property (nonatomic, copy) RBLPopoverDelegateBlock didShowBlock; 105 | 106 | // Use for animation when showing and closing the popover. 107 | @property (nonatomic, assign) NSTimeInterval fadeDuration; 108 | 109 | // Whether the popover can become the key window. 110 | // 111 | // Defaults to `NO`. 112 | @property (nonatomic, assign) BOOL canBecomeKey; 113 | 114 | // Returns a newly initialised `RBLPopover` with a `RBLPopoverBackgroundView`. 115 | - (instancetype)initWithContentViewController:(NSViewController *)viewController; 116 | 117 | // Designated initialiser. 118 | // 119 | // Returns a newly initialised `RBLPopover`. 120 | - (instancetype)initWithContentViewController:(NSViewController *)viewController backgroundView:(RBLPopoverBackgroundView *)backgroundView; 121 | 122 | // Displays the popover 123 | // 124 | // If the popover is already visible, this will move the popover to be 125 | // re-positioned with the given `positioningRect` and `prederredEdge`. 126 | // 127 | // positioningRect - The area which the popover should "cling" to. Given in 128 | // positioningView's coordinate space. 129 | // positioningView - The view which the positioningRect is relative to. 130 | // preferredEdge - The edge of positioningRect which the popover should 131 | // "cling" to. If the entire popover will not fit on the 132 | // screen when clinging to the 133 | // preferredEdge another edge will be used to ensure the 134 | // content is visible. In the event that no edge allows the 135 | // popover to fit on the screen, preferredEdge is used. 136 | - (void)showRelativeToRect:(CGRect)positioningRect ofView:(NSView *)positioningView preferredEdge:(CGRectEdge)preferredEdge; 137 | 138 | // Closes the popover with the `fadeDuration` (if the popover animates). 139 | - (void)close; 140 | 141 | // Convenience method exposed for nib files. 142 | - (IBAction)performClose:(id)sender; 143 | 144 | @end 145 | 146 | @interface RBLPopoverBackgroundView : NSView 147 | 148 | // Given a size of the content this should be overridden by subclasses to 149 | // describe how big the overall popover should be. 150 | // 151 | // contentSize - The size of the content contained within the popover. 152 | // popoverEdge - The edge that is adjacent to the `positioningRect`. 153 | // 154 | // Returns the overall size of the backgroundView as a `CGSize`. 155 | - (CGSize)sizeForBackgroundViewWithContentSize:(CGSize)contentSize popoverEdge:(CGRectEdge)popoverEdge; 156 | 157 | // Given a frame for the background this should be overridden by subclasses to 158 | // describe where the content should fit within the popover. 159 | // By default this sits the content in the frame of the background view whilst 160 | // nudging the content to make room for the arrow and a 1px border. 161 | // 162 | // frame - The frame of the `backgroundView`. 163 | // popoverEdge - The edge that is adjacent to the `positioningRect`. 164 | // 165 | // Returns the frame of the content relative to the given background view frame 166 | // as a `CGRect`. 167 | - (CGRect)contentViewFrameForBackgroundFrame:(CGRect)frame popoverEdge:(CGRectEdge)popoverEdge; 168 | 169 | // The outline shape of a popover. 170 | // This can be overridden by subclasses if they wish to change the shape of the 171 | // popover but still use the default drawing of a simple stroke and fill. 172 | // 173 | // popoverEdge - The edge that is adjacent to the `positioningRect`. 174 | // frame - The frame of the background view. 175 | // 176 | // Returns a `CGPathRef` of the outline of the background view. 177 | - (CGPathRef)newPopoverPathForEdge:(CGRectEdge)popoverEdge inFrame:(CGRect)frame; 178 | 179 | // The edge of the target view which the popover is appearing next to. This will 180 | // be set by the popover. 181 | @property (nonatomic, assign, readonly) CGRectEdge popoverEdge; 182 | 183 | // The rectangle, in screen coordinates, where the popover originated. This will 184 | // be set by the popover. 185 | @property (nonatomic, assign, readonly) NSRect popoverOrigin; 186 | 187 | // The size of the arrow used to indicate the origin of the popover. 188 | // 189 | // Note that the height will always be the distance from the view to the tip of 190 | // the arrow. 191 | @property (nonatomic, assign) CGSize arrowSize; 192 | 193 | // The color used to fill the shape of the background view. 194 | @property (nonatomic, strong) NSColor *fillColor; 195 | 196 | @end 197 | -------------------------------------------------------------------------------- /Demos/RBLPopoverDemo/RBLPopoverDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BE74216618BBBBBB0008F11D /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE74216518BBBBBB0008F11D /* Cocoa.framework */; }; 11 | BE74217018BBBBBB0008F11D /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = BE74216E18BBBBBB0008F11D /* InfoPlist.strings */; }; 12 | BE74217218BBBBBB0008F11D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = BE74217118BBBBBB0008F11D /* main.m */; }; 13 | BE74217918BBBBBB0008F11D /* RPDAppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = BE74217818BBBBBB0008F11D /* RPDAppDelegate.m */; }; 14 | BE74217C18BBBBBB0008F11D /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = BE74217A18BBBBBB0008F11D /* MainMenu.xib */; }; 15 | BE74217E18BBBBBB0008F11D /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BE74217D18BBBBBB0008F11D /* Images.xcassets */; }; 16 | BE7421A618BBBE360008F11D /* Rebel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE7421A118BBBE1F0008F11D /* Rebel.framework */; }; 17 | BE7421A818BBBE4D0008F11D /* Rebel.framework in Copy Frameworks */ = {isa = PBXBuildFile; fileRef = BE7421A118BBBE1F0008F11D /* Rebel.framework */; }; 18 | BE7421AB18BBC3860008F11D /* RPDContentViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = BE7421AA18BBC3860008F11D /* RPDContentViewController.m */; }; 19 | BE7421AE18BBC3A40008F11D /* RPDContentViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = BE7421AC18BBC3A40008F11D /* RPDContentViewController.xib */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | BE7421A018BBBE1F0008F11D /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = BE74219918BBBE1E0008F11D /* Rebel.xcodeproj */; 26 | proxyType = 2; 27 | remoteGlobalIDString = D09AE4DF15C5F45200ECAD10; 28 | remoteInfo = Rebel; 29 | }; 30 | BE7421A218BBBE1F0008F11D /* PBXContainerItemProxy */ = { 31 | isa = PBXContainerItemProxy; 32 | containerPortal = BE74219918BBBE1E0008F11D /* Rebel.xcodeproj */; 33 | proxyType = 2; 34 | remoteGlobalIDString = D09AE4F715C5F45300ECAD10; 35 | remoteInfo = RebelTests; 36 | }; 37 | BE7421A418BBBE300008F11D /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = BE74219918BBBE1E0008F11D /* Rebel.xcodeproj */; 40 | proxyType = 1; 41 | remoteGlobalIDString = D09AE4DE15C5F45200ECAD10; 42 | remoteInfo = Rebel; 43 | }; 44 | /* End PBXContainerItemProxy section */ 45 | 46 | /* Begin PBXCopyFilesBuildPhase section */ 47 | BE7421A718BBBE3D0008F11D /* Copy Frameworks */ = { 48 | isa = PBXCopyFilesBuildPhase; 49 | buildActionMask = 2147483647; 50 | dstPath = ""; 51 | dstSubfolderSpec = 10; 52 | files = ( 53 | BE7421A818BBBE4D0008F11D /* Rebel.framework in Copy Frameworks */, 54 | ); 55 | name = "Copy Frameworks"; 56 | runOnlyForDeploymentPostprocessing = 0; 57 | }; 58 | /* End PBXCopyFilesBuildPhase section */ 59 | 60 | /* Begin PBXFileReference section */ 61 | BE74216218BBBBBB0008F11D /* RBLPopoverDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = RBLPopoverDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 62 | BE74216518BBBBBB0008F11D /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 63 | BE74216818BBBBBB0008F11D /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; 64 | BE74216918BBBBBB0008F11D /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = System/Library/Frameworks/CoreData.framework; sourceTree = SDKROOT; }; 65 | BE74216A18BBBBBB0008F11D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 66 | BE74216D18BBBBBB0008F11D /* RBLPopoverDemo-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "RBLPopoverDemo-Info.plist"; sourceTree = ""; }; 67 | BE74216F18BBBBBB0008F11D /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 68 | BE74217118BBBBBB0008F11D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 69 | BE74217318BBBBBB0008F11D /* RBLPopoverDemo-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RBLPopoverDemo-Prefix.pch"; sourceTree = ""; }; 70 | BE74217718BBBBBB0008F11D /* RPDAppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RPDAppDelegate.h; sourceTree = ""; }; 71 | BE74217818BBBBBB0008F11D /* RPDAppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RPDAppDelegate.m; sourceTree = ""; }; 72 | BE74217B18BBBBBB0008F11D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; 73 | BE74217D18BBBBBB0008F11D /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 74 | BE74218418BBBBBB0008F11D /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 75 | BE74219918BBBE1E0008F11D /* Rebel.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Rebel.xcodeproj; path = ../../Rebel.xcodeproj; sourceTree = ""; }; 76 | BE7421A918BBC3860008F11D /* RPDContentViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RPDContentViewController.h; sourceTree = ""; }; 77 | BE7421AA18BBC3860008F11D /* RPDContentViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RPDContentViewController.m; sourceTree = ""; }; 78 | BE7421AD18BBC3A40008F11D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/RPDContentViewController.xib; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | BE74215F18BBBBBB0008F11D /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | BE7421A618BBBE360008F11D /* Rebel.framework in Frameworks */, 87 | BE74216618BBBBBB0008F11D /* Cocoa.framework in Frameworks */, 88 | ); 89 | runOnlyForDeploymentPostprocessing = 0; 90 | }; 91 | /* End PBXFrameworksBuildPhase section */ 92 | 93 | /* Begin PBXGroup section */ 94 | BE74215918BBBBBB0008F11D = { 95 | isa = PBXGroup; 96 | children = ( 97 | BE74216B18BBBBBB0008F11D /* RBLPopoverDemo */, 98 | BE74216418BBBBBB0008F11D /* Frameworks */, 99 | BE74216318BBBBBB0008F11D /* Products */, 100 | ); 101 | sourceTree = ""; 102 | }; 103 | BE74216318BBBBBB0008F11D /* Products */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | BE74216218BBBBBB0008F11D /* RBLPopoverDemo.app */, 107 | ); 108 | name = Products; 109 | sourceTree = ""; 110 | }; 111 | BE74216418BBBBBB0008F11D /* Frameworks */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | BE74219918BBBE1E0008F11D /* Rebel.xcodeproj */, 115 | BE74216518BBBBBB0008F11D /* Cocoa.framework */, 116 | BE74218418BBBBBB0008F11D /* XCTest.framework */, 117 | BE74216718BBBBBB0008F11D /* Other Frameworks */, 118 | ); 119 | name = Frameworks; 120 | sourceTree = ""; 121 | }; 122 | BE74216718BBBBBB0008F11D /* Other Frameworks */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | BE74216818BBBBBB0008F11D /* AppKit.framework */, 126 | BE74216918BBBBBB0008F11D /* CoreData.framework */, 127 | BE74216A18BBBBBB0008F11D /* Foundation.framework */, 128 | ); 129 | name = "Other Frameworks"; 130 | sourceTree = ""; 131 | }; 132 | BE74216B18BBBBBB0008F11D /* RBLPopoverDemo */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | BE74217718BBBBBB0008F11D /* RPDAppDelegate.h */, 136 | BE74217818BBBBBB0008F11D /* RPDAppDelegate.m */, 137 | BE7421A918BBC3860008F11D /* RPDContentViewController.h */, 138 | BE7421AA18BBC3860008F11D /* RPDContentViewController.m */, 139 | BE7421AC18BBC3A40008F11D /* RPDContentViewController.xib */, 140 | BE74217A18BBBBBB0008F11D /* MainMenu.xib */, 141 | BE74217D18BBBBBB0008F11D /* Images.xcassets */, 142 | BE74216C18BBBBBB0008F11D /* Supporting Files */, 143 | ); 144 | path = RBLPopoverDemo; 145 | sourceTree = ""; 146 | }; 147 | BE74216C18BBBBBB0008F11D /* Supporting Files */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | BE74216D18BBBBBB0008F11D /* RBLPopoverDemo-Info.plist */, 151 | BE74216E18BBBBBB0008F11D /* InfoPlist.strings */, 152 | BE74217118BBBBBB0008F11D /* main.m */, 153 | BE74217318BBBBBB0008F11D /* RBLPopoverDemo-Prefix.pch */, 154 | ); 155 | name = "Supporting Files"; 156 | sourceTree = ""; 157 | }; 158 | BE74219A18BBBE1E0008F11D /* Products */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | BE7421A118BBBE1F0008F11D /* Rebel.framework */, 162 | BE7421A318BBBE1F0008F11D /* RebelTests.octest */, 163 | ); 164 | name = Products; 165 | sourceTree = ""; 166 | }; 167 | /* End PBXGroup section */ 168 | 169 | /* Begin PBXNativeTarget section */ 170 | BE74216118BBBBBB0008F11D /* RBLPopoverDemo */ = { 171 | isa = PBXNativeTarget; 172 | buildConfigurationList = BE74219318BBBBBB0008F11D /* Build configuration list for PBXNativeTarget "RBLPopoverDemo" */; 173 | buildPhases = ( 174 | BE74215E18BBBBBB0008F11D /* Sources */, 175 | BE74215F18BBBBBB0008F11D /* Frameworks */, 176 | BE74216018BBBBBB0008F11D /* Resources */, 177 | BE7421A718BBBE3D0008F11D /* Copy Frameworks */, 178 | ); 179 | buildRules = ( 180 | ); 181 | dependencies = ( 182 | BE7421A518BBBE300008F11D /* PBXTargetDependency */, 183 | ); 184 | name = RBLPopoverDemo; 185 | productName = RBLPopoverDemo; 186 | productReference = BE74216218BBBBBB0008F11D /* RBLPopoverDemo.app */; 187 | productType = "com.apple.product-type.application"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | BE74215A18BBBBBB0008F11D /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | CLASSPREFIX = RPD; 196 | LastUpgradeCheck = 0500; 197 | ORGANIZATIONNAME = GitHub; 198 | }; 199 | buildConfigurationList = BE74215D18BBBBBB0008F11D /* Build configuration list for PBXProject "RBLPopoverDemo" */; 200 | compatibilityVersion = "Xcode 3.2"; 201 | developmentRegion = English; 202 | hasScannedForEncodings = 0; 203 | knownRegions = ( 204 | en, 205 | Base, 206 | ); 207 | mainGroup = BE74215918BBBBBB0008F11D; 208 | productRefGroup = BE74216318BBBBBB0008F11D /* Products */; 209 | projectDirPath = ""; 210 | projectReferences = ( 211 | { 212 | ProductGroup = BE74219A18BBBE1E0008F11D /* Products */; 213 | ProjectRef = BE74219918BBBE1E0008F11D /* Rebel.xcodeproj */; 214 | }, 215 | ); 216 | projectRoot = ""; 217 | targets = ( 218 | BE74216118BBBBBB0008F11D /* RBLPopoverDemo */, 219 | ); 220 | }; 221 | /* End PBXProject section */ 222 | 223 | /* Begin PBXReferenceProxy section */ 224 | BE7421A118BBBE1F0008F11D /* Rebel.framework */ = { 225 | isa = PBXReferenceProxy; 226 | fileType = wrapper.framework; 227 | path = Rebel.framework; 228 | remoteRef = BE7421A018BBBE1F0008F11D /* PBXContainerItemProxy */; 229 | sourceTree = BUILT_PRODUCTS_DIR; 230 | }; 231 | BE7421A318BBBE1F0008F11D /* RebelTests.octest */ = { 232 | isa = PBXReferenceProxy; 233 | fileType = wrapper.cfbundle; 234 | path = RebelTests.octest; 235 | remoteRef = BE7421A218BBBE1F0008F11D /* PBXContainerItemProxy */; 236 | sourceTree = BUILT_PRODUCTS_DIR; 237 | }; 238 | /* End PBXReferenceProxy section */ 239 | 240 | /* Begin PBXResourcesBuildPhase section */ 241 | BE74216018BBBBBB0008F11D /* Resources */ = { 242 | isa = PBXResourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | BE74217018BBBBBB0008F11D /* InfoPlist.strings in Resources */, 246 | BE7421AE18BBC3A40008F11D /* RPDContentViewController.xib in Resources */, 247 | BE74217E18BBBBBB0008F11D /* Images.xcassets in Resources */, 248 | BE74217C18BBBBBB0008F11D /* MainMenu.xib in Resources */, 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXResourcesBuildPhase section */ 253 | 254 | /* Begin PBXSourcesBuildPhase section */ 255 | BE74215E18BBBBBB0008F11D /* Sources */ = { 256 | isa = PBXSourcesBuildPhase; 257 | buildActionMask = 2147483647; 258 | files = ( 259 | BE74217218BBBBBB0008F11D /* main.m in Sources */, 260 | BE7421AB18BBC3860008F11D /* RPDContentViewController.m in Sources */, 261 | BE74217918BBBBBB0008F11D /* RPDAppDelegate.m in Sources */, 262 | ); 263 | runOnlyForDeploymentPostprocessing = 0; 264 | }; 265 | /* End PBXSourcesBuildPhase section */ 266 | 267 | /* Begin PBXTargetDependency section */ 268 | BE7421A518BBBE300008F11D /* PBXTargetDependency */ = { 269 | isa = PBXTargetDependency; 270 | name = Rebel; 271 | targetProxy = BE7421A418BBBE300008F11D /* PBXContainerItemProxy */; 272 | }; 273 | /* End PBXTargetDependency section */ 274 | 275 | /* Begin PBXVariantGroup section */ 276 | BE74216E18BBBBBB0008F11D /* InfoPlist.strings */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | BE74216F18BBBBBB0008F11D /* en */, 280 | ); 281 | name = InfoPlist.strings; 282 | sourceTree = ""; 283 | }; 284 | BE74217A18BBBBBB0008F11D /* MainMenu.xib */ = { 285 | isa = PBXVariantGroup; 286 | children = ( 287 | BE74217B18BBBBBB0008F11D /* Base */, 288 | ); 289 | name = MainMenu.xib; 290 | sourceTree = ""; 291 | }; 292 | BE7421AC18BBC3A40008F11D /* RPDContentViewController.xib */ = { 293 | isa = PBXVariantGroup; 294 | children = ( 295 | BE7421AD18BBC3A40008F11D /* Base */, 296 | ); 297 | name = RPDContentViewController.xib; 298 | sourceTree = ""; 299 | }; 300 | /* End PBXVariantGroup section */ 301 | 302 | /* Begin XCBuildConfiguration section */ 303 | BE74219118BBBBBB0008F11D /* Debug */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | ALWAYS_SEARCH_USER_PATHS = NO; 307 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 308 | CLANG_CXX_LIBRARY = "libc++"; 309 | CLANG_ENABLE_OBJC_ARC = YES; 310 | CLANG_WARN_BOOL_CONVERSION = YES; 311 | CLANG_WARN_CONSTANT_CONVERSION = YES; 312 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 313 | CLANG_WARN_EMPTY_BODY = YES; 314 | CLANG_WARN_ENUM_CONVERSION = YES; 315 | CLANG_WARN_INT_CONVERSION = YES; 316 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 318 | COPY_PHASE_STRIP = NO; 319 | GCC_C_LANGUAGE_STANDARD = gnu99; 320 | GCC_DYNAMIC_NO_PIC = NO; 321 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 322 | GCC_OPTIMIZATION_LEVEL = 0; 323 | GCC_PREPROCESSOR_DEFINITIONS = ( 324 | "DEBUG=1", 325 | "$(inherited)", 326 | ); 327 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 328 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 329 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 330 | GCC_WARN_UNDECLARED_SELECTOR = YES; 331 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 332 | GCC_WARN_UNUSED_FUNCTION = YES; 333 | GCC_WARN_UNUSED_VARIABLE = YES; 334 | MACOSX_DEPLOYMENT_TARGET = 10.9; 335 | ONLY_ACTIVE_ARCH = YES; 336 | SDKROOT = macosx; 337 | }; 338 | name = Debug; 339 | }; 340 | BE74219218BBBBBB0008F11D /* Release */ = { 341 | isa = XCBuildConfiguration; 342 | buildSettings = { 343 | ALWAYS_SEARCH_USER_PATHS = NO; 344 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 345 | CLANG_CXX_LIBRARY = "libc++"; 346 | CLANG_ENABLE_OBJC_ARC = YES; 347 | CLANG_WARN_BOOL_CONVERSION = YES; 348 | CLANG_WARN_CONSTANT_CONVERSION = YES; 349 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 350 | CLANG_WARN_EMPTY_BODY = YES; 351 | CLANG_WARN_ENUM_CONVERSION = YES; 352 | CLANG_WARN_INT_CONVERSION = YES; 353 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 354 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 355 | COPY_PHASE_STRIP = YES; 356 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 357 | ENABLE_NS_ASSERTIONS = NO; 358 | GCC_C_LANGUAGE_STANDARD = gnu99; 359 | GCC_ENABLE_OBJC_EXCEPTIONS = YES; 360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 362 | GCC_WARN_UNDECLARED_SELECTOR = YES; 363 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 364 | GCC_WARN_UNUSED_FUNCTION = YES; 365 | GCC_WARN_UNUSED_VARIABLE = YES; 366 | MACOSX_DEPLOYMENT_TARGET = 10.9; 367 | SDKROOT = macosx; 368 | }; 369 | name = Release; 370 | }; 371 | BE74219418BBBBBB0008F11D /* Debug */ = { 372 | isa = XCBuildConfiguration; 373 | buildSettings = { 374 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 375 | COMBINE_HIDPI_IMAGES = YES; 376 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 377 | GCC_PREFIX_HEADER = "RBLPopoverDemo/RBLPopoverDemo-Prefix.pch"; 378 | INFOPLIST_FILE = "RBLPopoverDemo/RBLPopoverDemo-Info.plist"; 379 | PRODUCT_NAME = "$(TARGET_NAME)"; 380 | WRAPPER_EXTENSION = app; 381 | }; 382 | name = Debug; 383 | }; 384 | BE74219518BBBBBB0008F11D /* Release */ = { 385 | isa = XCBuildConfiguration; 386 | buildSettings = { 387 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 388 | COMBINE_HIDPI_IMAGES = YES; 389 | GCC_PRECOMPILE_PREFIX_HEADER = YES; 390 | GCC_PREFIX_HEADER = "RBLPopoverDemo/RBLPopoverDemo-Prefix.pch"; 391 | INFOPLIST_FILE = "RBLPopoverDemo/RBLPopoverDemo-Info.plist"; 392 | PRODUCT_NAME = "$(TARGET_NAME)"; 393 | WRAPPER_EXTENSION = app; 394 | }; 395 | name = Release; 396 | }; 397 | /* End XCBuildConfiguration section */ 398 | 399 | /* Begin XCConfigurationList section */ 400 | BE74215D18BBBBBB0008F11D /* Build configuration list for PBXProject "RBLPopoverDemo" */ = { 401 | isa = XCConfigurationList; 402 | buildConfigurations = ( 403 | BE74219118BBBBBB0008F11D /* Debug */, 404 | BE74219218BBBBBB0008F11D /* Release */, 405 | ); 406 | defaultConfigurationIsVisible = 0; 407 | defaultConfigurationName = Release; 408 | }; 409 | BE74219318BBBBBB0008F11D /* Build configuration list for PBXNativeTarget "RBLPopoverDemo" */ = { 410 | isa = XCConfigurationList; 411 | buildConfigurations = ( 412 | BE74219418BBBBBB0008F11D /* Debug */, 413 | BE74219518BBBBBB0008F11D /* Release */, 414 | ); 415 | defaultConfigurationIsVisible = 0; 416 | defaultConfigurationName = Release; 417 | }; 418 | /* End XCConfigurationList section */ 419 | }; 420 | rootObject = BE74215A18BBBBBB0008F11D /* Project object */; 421 | } 422 | -------------------------------------------------------------------------------- /Rebel/RBLPopover.m: -------------------------------------------------------------------------------- 1 | // 2 | // RBLPopover.m 3 | // Rebel 4 | // 5 | // Created by Danny Greg on 13/09/2012. 6 | // Copyright (c) 2012 GitHub. All rights reserved. 7 | // 8 | 9 | #import "RBLPopover.h" 10 | 11 | #import "NSColor+RBLCGColorAdditions.h" 12 | #import "NSView+RBLAnimationAdditions.h" 13 | 14 | //*************************************************************************** 15 | 16 | // Returns the median X value of the shared segment of the X edges of the given rects 17 | static CGFloat RBLRectsGetMedianX(CGRect r1, CGRect r2) { 18 | CGFloat minX = fmax(NSMinX(r1), NSMinX(r2)); 19 | CGFloat maxX = fmin(NSMaxX(r1), NSMaxX(r2)); 20 | return (minX + maxX) / 2; 21 | } 22 | 23 | // Returns the median X value of the shared segment of the X edges of the given rects 24 | static CGFloat RBLRectsGetMedianY(CGRect r1, CGRect r2) { 25 | CGFloat minY = fmax(NSMinY(r1), NSMinY(r2)); 26 | CGFloat maxY = fmin(NSMaxY(r1), NSMaxY(r2)); 27 | return (minY + maxY) / 2; 28 | } 29 | 30 | //*************************************************************************** 31 | 32 | // A class which forcably draws `NSClearColor.clearColor` around a given path, 33 | // effectively clipping any views to the path. You can think of it like a 34 | // `maskLayer` on a `CALayer`. 35 | @interface RBLPopoverClippingView : NSView 36 | 37 | // The path which the view will clip to. The clippingPath will be retained and 38 | // released automatically. 39 | @property (nonatomic) CGPathRef clippingPath; 40 | 41 | @end 42 | 43 | //*************************************************************************** 44 | 45 | // We'll use this as RBLPopover's backing window. Since it's borderless, we 46 | // just override the `isKeyWindow` method to make it behave in the way that 47 | // `canBecomeKey` demands. 48 | @interface RBLPopoverWindow : NSWindow 49 | 50 | @property (nonatomic) BOOL canBeKey; 51 | 52 | @end 53 | 54 | //*************************************************************************** 55 | 56 | @interface RBLPopoverBackgroundView () 57 | 58 | // The clipping view that's used to shape the popover to the correct path. This 59 | // property is prefixed because it's private and this class is meant to be 60 | // subclassed. 61 | @property (nonatomic, strong, readonly) RBLPopoverClippingView *rbl_clippingView; 62 | 63 | @property (nonatomic, assign, readwrite) CGRectEdge popoverEdge; 64 | 65 | @property (nonatomic, assign, readwrite) NSRect popoverOrigin; 66 | 67 | - (CGRectEdge)rbl_arrowEdgeForPopoverEdge:(CGRectEdge)popoverEdge; 68 | 69 | @end 70 | 71 | //*************************************************************************** 72 | 73 | @interface RBLPopover () 74 | 75 | // The window we are using to display the popover. 76 | @property (nonatomic, strong) RBLPopoverWindow *popoverWindow; 77 | 78 | // The identifier for the event monitor we are using to watch for mouse clicks 79 | // outisde of the popover. 80 | // We are not responsible for its memory management. 81 | @property (nonatomic, copy) NSSet *transientEventMonitors; 82 | 83 | // An opaque object for observing `NSWindowWillEnterFullScreenNotification` notifications. 84 | @property (nonatomic, strong) id willEnterFullScreenObserver; 85 | 86 | // An opaque object for observing `NSWindowWillExitFullScreenNotification` notifications. 87 | @property (nonatomic, strong) id willExitFullScreenObserver; 88 | 89 | // The size the content view was before the popover was shown. 90 | @property (nonatomic) CGSize originalViewSize; 91 | 92 | // Correctly removes our event monitor watching for mouse clicks external to the 93 | // popover. 94 | - (void)removeEventMonitors; 95 | 96 | @end 97 | 98 | //*************************************************************************** 99 | 100 | @implementation RBLPopoverClippingView 101 | 102 | - (void)dealloc { 103 | self.clippingPath = NULL; 104 | } 105 | 106 | - (NSView *)hitTest:(NSPoint)aPoint { 107 | return nil; 108 | } 109 | 110 | - (void)setClippingPath:(CGPathRef)clippingPath { 111 | if (clippingPath == _clippingPath) return; 112 | 113 | CGPathRelease(_clippingPath); 114 | _clippingPath = clippingPath; 115 | CGPathRetain(_clippingPath); 116 | 117 | self.needsDisplay = YES; 118 | } 119 | 120 | - (void)drawRect:(NSRect)dirtyRect { 121 | if (self.clippingPath == NULL) return; 122 | 123 | CGContextRef currentContext = NSGraphicsContext.currentContext.graphicsPort; 124 | CGContextAddRect(currentContext, self.bounds); 125 | CGContextAddPath(currentContext, self.clippingPath); 126 | CGContextSetBlendMode(currentContext, kCGBlendModeCopy); 127 | [NSColor.clearColor set]; 128 | CGContextEOFillPath(currentContext); 129 | } 130 | 131 | @end 132 | 133 | //*************************************************************************** 134 | 135 | @implementation RBLPopoverWindow 136 | 137 | - (BOOL)canBecomeKeyWindow { 138 | return self.canBeKey; 139 | } 140 | 141 | @end 142 | 143 | //*************************************************************************** 144 | 145 | @implementation RBLPopover 146 | 147 | - (instancetype)initWithContentViewController:(NSViewController *)viewController { 148 | RBLPopoverBackgroundView *view = [[RBLPopoverBackgroundView alloc] initWithFrame:NSZeroRect]; 149 | return [self initWithContentViewController:viewController backgroundView:view]; 150 | } 151 | 152 | - (instancetype)initWithContentViewController:(NSViewController *)viewController backgroundView:(RBLPopoverBackgroundView *)backgroundView { 153 | self = [super init]; 154 | if (self == nil) 155 | return nil; 156 | 157 | _anchorPoint = CGPointMake(0.5, 0.5); 158 | _contentViewController = viewController; 159 | _backgroundView = backgroundView; 160 | _behavior = RBLPopoverBehaviorApplicationDefined; 161 | _animates = YES; 162 | _fadeDuration = 0.3; 163 | 164 | return self; 165 | } 166 | 167 | - (void)dealloc { 168 | [self.popoverWindow close]; 169 | } 170 | 171 | #pragma mark - 172 | #pragma mark Derived Properties 173 | 174 | - (BOOL)isShown { 175 | return self.popoverWindow.isVisible; 176 | } 177 | 178 | #pragma mark - 179 | #pragma mark Showing 180 | 181 | - (void)showRelativeToRect:(CGRect)positioningRect ofView:(NSView *)positioningView preferredEdge:(CGRectEdge)preferredEdge { 182 | if (CGRectEqualToRect(positioningRect, CGRectZero)) { 183 | positioningRect = [positioningView bounds]; 184 | } 185 | 186 | NSRect windowRelativeRect = [positioningView convertRect:[positioningView alignmentRectForFrame:positioningRect] toView:nil]; 187 | CGRect screenRect = [positioningView.window convertRectToScreen:windowRelativeRect]; 188 | 189 | self.backgroundView.popoverOrigin = screenRect; 190 | 191 | self.originalViewSize = self.contentViewController.view.frame.size; 192 | CGSize contentViewSize = (CGSizeEqualToSize(self.contentSize, CGSizeZero) ? self.contentViewController.view.frame.size : self.contentSize); 193 | 194 | CGPoint anchorPoint = self.anchorPoint; 195 | CGRect (^popoverRectForEdge)(CGRectEdge) = ^(CGRectEdge popoverEdge) { 196 | CGSize popoverSize = [self.backgroundView sizeForBackgroundViewWithContentSize:contentViewSize popoverEdge:popoverEdge]; 197 | CGRect returnRect = NSMakeRect(0.0, 0.0, popoverSize.width, popoverSize.height); 198 | 199 | // In all the cases below, find the minimum and maximum position of the 200 | // popover and then use the anchor point to determine where the popover 201 | // should be between these two locations. 202 | // 203 | // `x0` indicates the x origin of the popover if `self.anchorPoint.x` is 204 | // 0 and aligns the left edge of the popover to the left edge of the 205 | // origin view. `x1` is the x origin if `self.anchorPoint.x` is 1 and 206 | // aligns the right edge of the popover to the right edge of the origin 207 | // view. The anchor point determines where the popover should be between 208 | // these extremes. 209 | if (popoverEdge == CGRectMinYEdge) { 210 | CGFloat x0 = NSMinX(screenRect); 211 | CGFloat x1 = NSMaxX(screenRect) - contentViewSize.width; 212 | returnRect.origin.x = x0 + floor((x1 - x0) * anchorPoint.x); 213 | returnRect.origin.y = NSMinY(screenRect) - popoverSize.height; 214 | } else if (popoverEdge == CGRectMaxYEdge) { 215 | CGFloat x0 = NSMinX(screenRect); 216 | CGFloat x1 = NSMaxX(screenRect) - contentViewSize.width; 217 | returnRect.origin.x = x0 + floor((x1 - x0) * anchorPoint.x); 218 | returnRect.origin.y = NSMaxY(screenRect); 219 | } else if (popoverEdge == CGRectMinXEdge) { 220 | CGFloat y0 = NSMinY(screenRect); 221 | CGFloat y1 = NSMaxY(screenRect) - contentViewSize.height; 222 | returnRect.origin.x = NSMinX(screenRect) - popoverSize.width; 223 | returnRect.origin.y = y0 + floor((y1 - y0) * anchorPoint.y); 224 | } else if (popoverEdge == CGRectMaxXEdge) { 225 | CGFloat y0 = NSMinY(screenRect); 226 | CGFloat y1 = NSMaxY(screenRect) - contentViewSize.height; 227 | returnRect.origin.x = NSMaxX(screenRect); 228 | returnRect.origin.y = y0 + floor((y1 - y0) * anchorPoint.y); 229 | } else { 230 | returnRect = CGRectZero; 231 | } 232 | 233 | return returnRect; 234 | }; 235 | 236 | BOOL (^checkPopoverSizeForScreenWithPopoverEdge)(CGRectEdge) = ^(CGRectEdge popoverEdge) { 237 | CGRect popoverRect = popoverRectForEdge(popoverEdge); 238 | return NSContainsRect(positioningView.window.screen.visibleFrame, popoverRect); 239 | }; 240 | 241 | //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 behavior 242 | __block CGRectEdge popoverEdge = preferredEdge; 243 | CGRect (^popoverRect)() = ^{ 244 | CGRectEdge (^nextEdgeForEdge)(CGRectEdge) = ^CGRectEdge (CGRectEdge currentEdge) 245 | { 246 | if (currentEdge == CGRectMaxXEdge) { 247 | return (preferredEdge == CGRectMinXEdge ? CGRectMaxYEdge : CGRectMinXEdge); 248 | } else if (currentEdge == CGRectMinXEdge) { 249 | return (preferredEdge == CGRectMaxXEdge ? CGRectMaxYEdge : CGRectMaxXEdge); 250 | } else if (currentEdge == CGRectMaxYEdge) { 251 | return (preferredEdge == CGRectMinYEdge ? CGRectMaxXEdge : CGRectMinYEdge); 252 | } else if (currentEdge == CGRectMinYEdge) { 253 | return (preferredEdge == CGRectMaxYEdge ? CGRectMaxXEdge : CGRectMaxYEdge); 254 | } 255 | 256 | return currentEdge; 257 | }; 258 | 259 | CGRect (^fitRectToScreen)(CGRect) = ^CGRect (CGRect proposedRect) { 260 | NSRect screenRect = positioningView.window.screen.visibleFrame; 261 | 262 | if (proposedRect.origin.y < NSMinY(screenRect)) { 263 | proposedRect.origin.y = NSMinY(screenRect); 264 | } 265 | if (proposedRect.origin.x < NSMinX(screenRect)) { 266 | proposedRect.origin.x = NSMinX(screenRect); 267 | } 268 | 269 | if (NSMaxY(proposedRect) > NSMaxY(screenRect)) { 270 | proposedRect.origin.y = (NSMaxY(screenRect) - NSHeight(proposedRect)); 271 | } 272 | if (NSMaxX(proposedRect) > NSMaxX(screenRect)) { 273 | proposedRect.origin.x = (NSMaxX(screenRect) - NSWidth(proposedRect)); 274 | } 275 | 276 | return proposedRect; 277 | }; 278 | 279 | BOOL (^screenRectContainsRectEdge)(CGRectEdge) = ^ BOOL (CGRectEdge edge) { 280 | CGRect proposedRect = popoverRectForEdge(edge); 281 | NSRect screenRect = positioningView.window.screen.visibleFrame; 282 | 283 | BOOL minYInBounds = (edge == CGRectMinYEdge && NSMinY(proposedRect) >= NSMinY(screenRect)); 284 | BOOL maxYInBounds = (edge == CGRectMaxYEdge && NSMaxY(proposedRect) <= NSMaxY(screenRect)); 285 | BOOL minXInBounds = (edge == CGRectMinXEdge && NSMinX(proposedRect) >= NSMinX(screenRect)); 286 | BOOL maxXInBounds = (edge == CGRectMaxXEdge && NSMaxX(proposedRect) <= NSMaxX(screenRect)); 287 | 288 | return minYInBounds || maxYInBounds || minXInBounds || maxXInBounds; 289 | }; 290 | 291 | NSUInteger attemptCount = 0; 292 | while (!checkPopoverSizeForScreenWithPopoverEdge(popoverEdge)) { 293 | if (attemptCount >= 4) { 294 | popoverEdge = (screenRectContainsRectEdge(preferredEdge) ? preferredEdge : nextEdgeForEdge(preferredEdge)); 295 | 296 | return fitRectToScreen(popoverRectForEdge(popoverEdge)); 297 | break; 298 | } 299 | 300 | popoverEdge = nextEdgeForEdge(popoverEdge); 301 | attemptCount ++; 302 | } 303 | 304 | return popoverRectForEdge(popoverEdge); 305 | }; 306 | 307 | CGRect popoverScreenRect = popoverRect(); 308 | 309 | if (self.shown) { 310 | if (self.backgroundView.popoverEdge == popoverEdge) { 311 | CGSize size = [self.backgroundView sizeForBackgroundViewWithContentSize:contentViewSize popoverEdge:popoverEdge]; 312 | self.backgroundView.frame = (NSRect){ .size = size }; 313 | [self.popoverWindow setFrame:popoverScreenRect display:YES]; 314 | 315 | return; 316 | } 317 | 318 | [self.popoverWindow close]; 319 | self.popoverWindow = nil; 320 | } 321 | 322 | //TODO: Create RBLViewController with viewWillAppear 323 | //[self.contentViewController viewWillAppear:YES]; //this will always be animated… in the current implementation 324 | 325 | if (self.willShowBlock != nil) self.willShowBlock(self); 326 | 327 | if (self.behavior != NSPopoverBehaviorApplicationDefined) { 328 | [self removeEventMonitors]; 329 | 330 | __weak RBLPopover *weakSelf = self; 331 | void (^monitor)(NSEvent *event) = ^(NSEvent *event) { 332 | RBLPopover *strongSelf = weakSelf; 333 | if (strongSelf.popoverWindow == nil) return; 334 | BOOL shouldClose = NO; 335 | BOOL mouseInPopoverWindow = ([NSWindow windowNumberAtPoint:NSEvent.mouseLocation belowWindowWithWindowNumber:0] == strongSelf.popoverWindow.windowNumber); 336 | if (strongSelf.behavior == RBLPopoverBehaviorTransient) { 337 | shouldClose = !mouseInPopoverWindow; 338 | } else { 339 | NSWindow *parentWindow = strongSelf.popoverWindow.parentWindow; 340 | BOOL inParentWindow = ([NSWindow windowNumberAtPoint:NSEvent.mouseLocation belowWindowWithWindowNumber:0] == parentWindow.windowNumber); 341 | 342 | shouldClose = inParentWindow && !mouseInPopoverWindow; 343 | } 344 | 345 | if (shouldClose) [strongSelf close]; 346 | }; 347 | 348 | NSInteger mask = 0; 349 | if (self.behavior == RBLPopoverBehaviorTransient) { 350 | mask = NSLeftMouseDownMask | NSRightMouseDownMask; 351 | } else { 352 | mask = NSLeftMouseUpMask | NSRightMouseUpMask; 353 | } 354 | 355 | NSMutableSet *newMonitors = [[NSMutableSet alloc] init]; 356 | if (self.behavior == RBLPopoverBehaviorTransient) { 357 | [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(appResignedActive:) name:NSApplicationDidResignActiveNotification object:NSApp]; 358 | id globalMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:mask handler:monitor]; 359 | [newMonitors addObject:globalMonitor]; 360 | } 361 | 362 | id localMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:mask handler:^(NSEvent *event) { 363 | monitor(event); 364 | return event; 365 | }]; 366 | [newMonitors addObject:localMonitor]; 367 | self.transientEventMonitors = newMonitors; 368 | } 369 | 370 | CGSize size = [self.backgroundView sizeForBackgroundViewWithContentSize:contentViewSize popoverEdge:popoverEdge]; 371 | self.backgroundView.frame = (NSRect){ .size = size }; 372 | self.backgroundView.popoverEdge = popoverEdge; 373 | 374 | CGRect contentViewFrame = [self.backgroundView contentViewFrameForBackgroundFrame:self.backgroundView.bounds popoverEdge:popoverEdge]; 375 | self.contentViewController.view.autoresizingMask = (NSViewWidthSizable | NSViewHeightSizable); 376 | self.contentViewController.view.frame = contentViewFrame; 377 | [self.backgroundView addSubview:self.contentViewController.view positioned:NSWindowBelow relativeTo:nil]; 378 | self.popoverWindow = [[RBLPopoverWindow alloc] initWithContentRect:popoverScreenRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]; 379 | self.popoverWindow.hasShadow = YES; 380 | self.popoverWindow.releasedWhenClosed = NO; 381 | self.popoverWindow.opaque = NO; 382 | self.popoverWindow.backgroundColor = NSColor.clearColor; 383 | self.popoverWindow.contentView = self.backgroundView; 384 | self.popoverWindow.canBeKey = self.canBecomeKey; 385 | if (self.animates) { 386 | self.popoverWindow.alphaValue = 0.0; 387 | } 388 | 389 | // We're using a dummy button to capture the escape key equivalent when it 390 | // isn't handled by the first responder. This is bad and I feel bad. 391 | NSButton *closeButton = [[NSButton alloc] initWithFrame:CGRectMake(-1, -1, 0, 0)]; 392 | closeButton.keyEquivalent = @"\E"; 393 | closeButton.target = self; 394 | closeButton.action = @selector(performClose:); 395 | [self.popoverWindow.contentView addSubview:closeButton]; 396 | 397 | NSWindow *topmostParentWindow = positioningView.window; 398 | while (topmostParentWindow.parentWindow != nil) { 399 | topmostParentWindow = topmostParentWindow.parentWindow; 400 | } 401 | 402 | if (self.behavior != RBLPopoverBehaviorApplicationDefined) { 403 | __weak typeof(self) weakSelf = self; 404 | void(^closePopoverBlock)(NSNotification *) = ^(NSNotification *note) { 405 | __strong typeof(weakSelf) popover = weakSelf; 406 | // Turn off animations. We want the close to be instantaneous since the 407 | // parent window's going to be animating too. 408 | BOOL shouldAnimate = popover.animates; 409 | popover.animates = NO; 410 | [popover close]; 411 | popover.animates = shouldAnimate; 412 | }; 413 | 414 | self.willEnterFullScreenObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowWillEnterFullScreenNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:closePopoverBlock]; 415 | self.willExitFullScreenObserver = [NSNotificationCenter.defaultCenter addObserverForName:NSWindowWillExitFullScreenNotification object:nil queue:NSOperationQueue.mainQueue usingBlock:closePopoverBlock]; 416 | } 417 | 418 | [topmostParentWindow addChildWindow:self.popoverWindow ordered:NSWindowAbove]; 419 | [self.popoverWindow makeKeyAndOrderFront:self]; 420 | 421 | void (^postDisplayBlock)(void) = ^{ 422 | if (self.didShowBlock != NULL) self.didShowBlock(self); 423 | }; 424 | 425 | if (self.animates) { 426 | [NSView rbl_animateWithDuration:self.fadeDuration animations:^{ 427 | [self.popoverWindow.animator setAlphaValue:1.0]; 428 | } completion:postDisplayBlock]; 429 | } else { 430 | postDisplayBlock(); 431 | } 432 | } 433 | 434 | #pragma mark - 435 | #pragma mark Closing 436 | 437 | - (void)close { 438 | if (!self.shown) return; 439 | 440 | [self removeEventMonitors]; 441 | 442 | if (self.willCloseBlock != nil) self.willCloseBlock(self); 443 | 444 | void (^windowTeardown)(void) = ^{ 445 | [self.popoverWindow.parentWindow removeChildWindow:self.popoverWindow]; 446 | [self.popoverWindow close]; 447 | 448 | if (self.didCloseBlock != nil) self.didCloseBlock(self); 449 | 450 | self.contentViewController.view.frame = CGRectMake(self.contentViewController.view.frame.origin.x, self.contentViewController.view.frame.origin.y, self.originalViewSize.width, self.originalViewSize.height); 451 | }; 452 | 453 | if (self.animates) { 454 | [NSView rbl_animateWithDuration:self.fadeDuration animations:^{ 455 | [self.popoverWindow.animator setAlphaValue:0.0]; 456 | } completion:windowTeardown]; 457 | } else { 458 | windowTeardown(); 459 | } 460 | } 461 | 462 | - (IBAction)performClose:(id)sender { 463 | [self close]; 464 | } 465 | 466 | #pragma mark - 467 | #pragma mark Event Monitor 468 | 469 | - (void)removeEventMonitors { 470 | for (id eventMonitor in self.transientEventMonitors) { 471 | [NSEvent removeMonitor:eventMonitor]; 472 | } 473 | self.transientEventMonitors = nil; 474 | [NSNotificationCenter.defaultCenter removeObserver:self name:NSApplicationDidResignActiveNotification object:NSApp]; 475 | 476 | if (self.willEnterFullScreenObserver != nil && self.willExitFullScreenObserver != nil) { 477 | [NSNotificationCenter.defaultCenter removeObserver:self.willEnterFullScreenObserver]; 478 | [NSNotificationCenter.defaultCenter removeObserver:self.willExitFullScreenObserver]; 479 | self.willEnterFullScreenObserver = nil; 480 | self.willExitFullScreenObserver = nil; 481 | } 482 | } 483 | 484 | - (void)appResignedActive:(NSNotification *)notification { 485 | if (self.behavior == RBLPopoverBehaviorTransient) [self close]; 486 | } 487 | 488 | @end 489 | 490 | //*************************************************************************** 491 | 492 | static CGFloat const RBLPopoverBackgroundViewBorderRadius = 5.0; 493 | static CGFloat const RBLPopoverBackgroundViewArrowHeight = 17.0; 494 | static CGFloat const RBLPopoverBackgroundViewArrowWidth = 35.0; 495 | 496 | //*************************************************************************** 497 | 498 | @implementation RBLPopoverBackgroundView 499 | 500 | - (CGSize)sizeForBackgroundViewWithContentSize:(CGSize)contentSize popoverEdge:(CGRectEdge)popoverEdge { 501 | CGSize returnSize = contentSize; 502 | if (popoverEdge == CGRectMaxXEdge || popoverEdge == CGRectMinXEdge) { 503 | returnSize.width += self.arrowSize.height; 504 | } else { 505 | returnSize.height += self.arrowSize.height; 506 | } 507 | 508 | returnSize.width += 2.0; 509 | returnSize.height += 2.0; 510 | 511 | return returnSize; 512 | } 513 | 514 | - (CGRect)contentViewFrameForBackgroundFrame:(CGRect)backgroundFrame popoverEdge:(CGRectEdge)popoverEdge { 515 | CGRect returnFrame = NSInsetRect(backgroundFrame, 1.0, 1.0); 516 | switch (popoverEdge) { 517 | case CGRectMinXEdge: 518 | returnFrame.size.width -= self.arrowSize.height; 519 | break; 520 | case CGRectMinYEdge: 521 | returnFrame.size.height -= self.arrowSize.height; 522 | break; 523 | case CGRectMaxXEdge: 524 | returnFrame.size.width -= self.arrowSize.height; 525 | returnFrame.origin.x += self.arrowSize.height; 526 | break; 527 | case CGRectMaxYEdge: 528 | returnFrame.size.height -= self.arrowSize.height; 529 | returnFrame.origin.y += self.arrowSize.height; 530 | break; 531 | default: 532 | NSAssert(NO, @"Failed to pass in a valid CGRectEdge"); 533 | break; 534 | } 535 | 536 | return returnFrame; 537 | } 538 | 539 | - (CGPathRef)newPopoverPathForEdge:(CGRectEdge)popoverEdge inFrame:(CGRect)frame { 540 | CGRectEdge arrowEdge = [self rbl_arrowEdgeForPopoverEdge:popoverEdge]; 541 | 542 | CGRect contentRect = CGRectIntegral([self contentViewFrameForBackgroundFrame:frame popoverEdge:self.popoverEdge]); 543 | CGFloat minX = NSMinX(contentRect); 544 | CGFloat maxX = NSMaxX(contentRect); 545 | CGFloat minY = NSMinY(contentRect); 546 | CGFloat maxY = NSMaxY(contentRect); 547 | 548 | CGRect windowRect = [self.window convertRectFromScreen:self.popoverOrigin]; 549 | CGRect originRect = [self convertRect:windowRect fromView:nil]; 550 | CGFloat midOriginX = floor(RBLRectsGetMedianX(originRect, contentRect)); 551 | CGFloat midOriginY = floor(RBLRectsGetMedianY(originRect, contentRect)); 552 | 553 | CGFloat maxArrowX = 0.0; 554 | CGFloat minArrowX = 0.0; 555 | CGFloat minArrowY = 0.0; 556 | CGFloat maxArrowY = 0.0; 557 | 558 | // Even I have no idea at this point… :trollface: 559 | // So we don't have a weird arrow situation we need to make sure we draw it within the radius. 560 | // If we have to nudge it then we have to shrink the arrow as otherwise it looks all wonky and weird. 561 | // That is what this complete mess below does. 562 | 563 | if (arrowEdge == CGRectMinYEdge || arrowEdge == CGRectMaxYEdge) { 564 | maxArrowX = floor(midOriginX + (self.arrowSize.width / 2.0)); 565 | CGFloat maxPossible = (NSMaxX(contentRect) - RBLPopoverBackgroundViewBorderRadius); 566 | if (maxArrowX > maxPossible) { 567 | maxArrowX = maxPossible; 568 | minArrowX = maxArrowX - self.arrowSize.width; 569 | } else { 570 | minArrowX = floor(midOriginX - (self.arrowSize.width / 2.0)); 571 | if (minArrowX < RBLPopoverBackgroundViewBorderRadius) { 572 | minArrowX = RBLPopoverBackgroundViewBorderRadius; 573 | maxArrowX = minArrowX + self.arrowSize.width; 574 | } 575 | } 576 | } else { 577 | minArrowY = floor(midOriginY - (self.arrowSize.width / 2.0)); 578 | if (minArrowY < RBLPopoverBackgroundViewBorderRadius) { 579 | minArrowY = RBLPopoverBackgroundViewBorderRadius; 580 | maxArrowY = minArrowY + self.arrowSize.width; 581 | } else { 582 | maxArrowY = floor(midOriginY + (self.arrowSize.width / 2.0)); 583 | CGFloat maxPossible = (NSMaxY(contentRect) - RBLPopoverBackgroundViewBorderRadius); 584 | if (maxArrowY > maxPossible) { 585 | maxArrowY = maxPossible; 586 | minArrowY = maxArrowY - self.arrowSize.width; 587 | } 588 | } 589 | } 590 | 591 | // These represent the centerpoints of the popover's corner arcs. 592 | CGFloat minCenterpointX = floor(minX + RBLPopoverBackgroundViewBorderRadius); 593 | CGFloat maxCenterpointX = floor(maxX - RBLPopoverBackgroundViewBorderRadius); 594 | CGFloat minCenterpointY = floor(minY + RBLPopoverBackgroundViewBorderRadius); 595 | CGFloat maxCenterpointY = floor(maxY - RBLPopoverBackgroundViewBorderRadius); 596 | 597 | CGMutablePathRef path = CGPathCreateMutable(); 598 | CGPathMoveToPoint(path, NULL, minX, minCenterpointY); 599 | 600 | CGFloat radius = RBLPopoverBackgroundViewBorderRadius; 601 | 602 | CGPathAddArc(path, NULL, minCenterpointX, maxCenterpointY, radius, M_PI, M_PI_2, true); 603 | 604 | CGPathAddArc(path, NULL, maxCenterpointX, maxCenterpointY, radius, M_PI_2, 0, true); 605 | 606 | CGPathAddArc(path, NULL, maxCenterpointX, minCenterpointY, radius, 0, -M_PI_2, true); 607 | 608 | CGPathAddArc(path, NULL, minCenterpointX, minCenterpointY, radius, -M_PI_2, M_PI, true); 609 | 610 | CGPoint minBasePoint, tipPoint, maxBasePoint; 611 | switch (arrowEdge) { 612 | case CGRectMinXEdge: 613 | minBasePoint = CGPointMake(minX, minArrowY); 614 | tipPoint = CGPointMake(floor(minX - self.arrowSize.height), floor((minArrowY + maxArrowY) / 2)); 615 | maxBasePoint = CGPointMake(minX, maxArrowY); 616 | break; 617 | case CGRectMaxYEdge: 618 | minBasePoint = CGPointMake(minArrowX, maxY); 619 | tipPoint = CGPointMake(floor((minArrowX + maxArrowX) / 2), floor(maxY + self.arrowSize.height)); 620 | maxBasePoint = CGPointMake(maxArrowX, maxY); 621 | break; 622 | case CGRectMaxXEdge: 623 | minBasePoint = CGPointMake(maxX, minArrowY); 624 | tipPoint = CGPointMake(floor(maxX + self.arrowSize.height), floor((minArrowY + maxArrowY) / 2)); 625 | maxBasePoint = CGPointMake(maxX, maxArrowY); 626 | break; 627 | case CGRectMinYEdge: 628 | minBasePoint = CGPointMake(minArrowX, minY); 629 | tipPoint = CGPointMake(floor((minArrowX + maxArrowX) / 2), floor(minY - self.arrowSize.height)); 630 | maxBasePoint = CGPointMake(maxArrowX, minY); 631 | break; 632 | default: 633 | break; 634 | } 635 | 636 | CGPathMoveToPoint(path, NULL, minBasePoint.x, minBasePoint.y); 637 | CGPathAddLineToPoint(path, NULL, tipPoint.x, tipPoint.y); 638 | CGPathAddLineToPoint(path, NULL, maxBasePoint.x, maxBasePoint.y); 639 | 640 | return path; 641 | } 642 | 643 | - (instancetype)initWithFrame:(CGRect)frame { 644 | self = [super initWithFrame:frame]; 645 | if (self == nil) return nil; 646 | 647 | _arrowSize = CGSizeMake(RBLPopoverBackgroundViewArrowWidth, RBLPopoverBackgroundViewArrowHeight); 648 | _fillColor = NSColor.whiteColor; 649 | 650 | _rbl_clippingView = [[RBLPopoverClippingView alloc] initWithFrame:self.bounds]; 651 | self.rbl_clippingView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; 652 | [self addSubview:self.rbl_clippingView]; 653 | 654 | return self; 655 | } 656 | 657 | - (void)setFrame:(NSRect)frameRect { 658 | [super setFrame:frameRect]; 659 | self.needsDisplay = YES; 660 | } 661 | 662 | - (void)setArrowSize:(CGSize)arrowSize { 663 | if (CGSizeEqualToSize(arrowSize, self.arrowSize)) return; 664 | _arrowSize = arrowSize; 665 | self.needsDisplay = YES; 666 | } 667 | 668 | - (void)setPopoverEdge:(CGRectEdge)popoverEdge { 669 | if (popoverEdge == self.popoverEdge) return; 670 | _popoverEdge = popoverEdge; 671 | self.needsDisplay = YES; 672 | } 673 | 674 | - (void)setPopoverOrigin:(NSRect)popoverOrigin { 675 | if (NSEqualRects(popoverOrigin, self.popoverOrigin)) return; 676 | _popoverOrigin = popoverOrigin; 677 | self.needsDisplay = YES; 678 | } 679 | 680 | - (void)drawRect:(NSRect)rect { 681 | [super drawRect:rect]; 682 | [self.fillColor set]; 683 | NSRectFill(rect); 684 | } 685 | 686 | - (BOOL)isOpaque { 687 | return NO; 688 | } 689 | 690 | - (void)viewWillDraw { 691 | [super viewWillDraw]; 692 | [self rbl_updateClippingView]; 693 | } 694 | 695 | #pragma mark - Private Methods 696 | 697 | - (CGRectEdge)rbl_arrowEdgeForPopoverEdge:(CGRectEdge)popoverEdge { 698 | CGRectEdge arrowEdge = CGRectMinYEdge; 699 | switch (popoverEdge) { 700 | case CGRectMaxXEdge: 701 | arrowEdge = CGRectMinXEdge; 702 | break; 703 | case CGRectMaxYEdge: 704 | arrowEdge = CGRectMinYEdge; 705 | break; 706 | case CGRectMinXEdge: 707 | arrowEdge = CGRectMaxXEdge; 708 | break; 709 | case CGRectMinYEdge: 710 | arrowEdge = CGRectMaxYEdge; 711 | break; 712 | default: 713 | break; 714 | } 715 | 716 | return arrowEdge; 717 | } 718 | 719 | - (void)rbl_updateClippingView { 720 | // There's no point if it's not in a window 721 | if (self.window == nil) return; 722 | CGPathRef clippingPath = [self newPopoverPathForEdge:self.popoverEdge inFrame:self.rbl_clippingView.bounds]; 723 | self.rbl_clippingView.clippingPath = clippingPath; 724 | CGPathRelease(clippingPath); 725 | } 726 | 727 | @end 728 | --------------------------------------------------------------------------------