Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ profile
DerivedData
.idea/
*.hmap

Demo.xcodeproj/project.xcworkspace/xcshareddata/Demo.xccheckout
32 changes: 28 additions & 4 deletions AXStatusItemPopup/AXStatusItemPopup.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,37 @@

#import <Cocoa/Cocoa.h>

@interface AXStatusItemPopup : NSView
@interface NSWindow (canBecomeKeyWindow)

@end

@protocol AXStatusItemPopupDelegate <NSObject>

@optional

- (BOOL)shouldPopupOpen;
- (void)popupWillOpen;
- (void)popupDidOpen;

- (BOOL)shouldPopupClose;
- (void)popupWillClose;
- (void)popupDidClose;

@end

@interface AXStatusItemPopup : NSView <NSPopoverDelegate>

// properties
@property(assign, nonatomic, getter=isActive) BOOL active;
@property(assign, nonatomic) BOOL animated;
@property(assign, nonatomic, getter=isAnimated) BOOL animated;
@property(strong, nonatomic) NSImage *image;
@property(strong, nonatomic) NSImage *alternateImage;
@property(strong, nonatomic) NSStatusItem *statusItem;
@property(weak) id<AXStatusItemPopupDelegate> delegate;


// alloc
+ (id)statusItemPopupWithViewController:(NSViewController *)controller;
+ (id)statusItemPopupWithViewController:(NSViewController *)controller image:(NSImage *)image;
+ (id)statusItemPopupWithViewController:(NSViewController *)controller image:(NSImage *)image alternateImage:(NSImage *)alternateImage;

// init
- (id)initWithViewController:(NSViewController *)controller;
Expand All @@ -25,6 +47,8 @@


// show / hide popover
- (void)togglePopover;
- (void)togglePopoverAnimated: (BOOL)animated;
- (void)showPopover;
- (void)showPopoverAnimated:(BOOL)animated;
- (void)hidePopover;
Expand Down
232 changes: 173 additions & 59 deletions AXStatusItemPopup/AXStatusItemPopup.m
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,50 @@

#define kMinViewWidth 22

BOOL shouldBecomeKeyWindow;
NSWindow *windowToOverride;

//
// Private variables
// Private properties
//
@interface AXStatusItemPopup () {
NSViewController *_viewController;
BOOL _active;
NSImageView *_imageView;
NSStatusItem *_statusItem;
NSPopover *_popover;
id _popoverTransiencyMonitor;
}
@interface AXStatusItemPopup ()
@property NSViewController *viewController;
@property NSImageView *imageView;
@property NSStatusItem *statusItem;
@property NSPopover *popover;
@property(assign, nonatomic, getter=isActive) BOOL active;

@end

///////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Implementation AXStatusItemPopup
/////////////////////////////////////////////////////////////////////////////////////

//
// Implementation
//
@implementation AXStatusItemPopup

/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Allocators
/////////////////////////////////////////////////////////////////////////////////////

+ (id)statusItemPopupWithViewController:(NSViewController *)controller
{
return [[self alloc] initWithViewController:controller];
}

+ (id)statusItemPopupWithViewController:(NSViewController *)controller image:(NSImage *)image
{
return [[self alloc] initWithViewController:controller image:image];
}

+ (id)statusItemPopupWithViewController:(NSViewController *)controller image:(NSImage *)image alternateImage:(NSImage *)alternateImage
{
return [[self alloc] initWithViewController:controller image:image alternateImage:alternateImage];
}

/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Initiators
/////////////////////////////////////////////////////////////////////////////////////

- (id)initWithViewController:(NSViewController *)controller
{
return [self initWithViewController:controller image:nil];
Expand All @@ -46,6 +70,8 @@ - (id)initWithViewController:(NSViewController *)controller image:(NSImage *)ima

self = [super initWithFrame:NSMakeRect(0, 0, kMinViewWidth, height)];
if (self) {
_active = NO;
_animated = YES;
_viewController = controller;

self.image = image;
Expand All @@ -56,54 +82,61 @@ - (id)initWithViewController:(NSViewController *)controller image:(NSImage *)ima

self.statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
self.statusItem.view = self;
self.statusItem.target = self;
self.statusItem.action = @selector(togglePopover:);

_active = NO;
_animated = YES;
self.popover = [[NSPopover alloc] init];
self.popover.contentViewController = self.viewController;
self.popover.animates = self.animated;
self.popover.delegate = self;

windowToOverride = self.window;

[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(applicationDidResignActive:) name:NSApplicationDidResignActiveNotification object:nil];
}

return self;
}


////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Drawing
////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

- (void)drawRect:(NSRect)dirtyRect
{
// set view background color
if (_active) {
if (self.isActive) {
[[NSColor selectedMenuItemColor] setFill];
} else {
[[NSColor clearColor] setFill];
}
NSRectFill(dirtyRect);

// set image
NSImage *image = (_active ? _alternateImage : _image);
NSImage *image = (self.isActive ? self.alternateImage : self.image);
_imageView.image = image;
}

////////////////////////////////////
#pragma mark - Mouse Actions
////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Mouse Events
/////////////////////////////////////////////////////////////////////////////////////

- (void)mouseDown:(NSEvent *)theEvent
{
if (_popover.isShown) {
[self hidePopover];
} else {
[self showPopover];
}
[self togglePopover];
}

////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Setter
////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

- (void)setActive:(BOOL)active
{
_active = active;
shouldBecomeKeyWindow = active;
[self setNeedsDisplay:YES];
[NSApp activateIgnoringOtherApps:active];
}

- (void)setImage:(NSImage *)image
Expand All @@ -121,59 +154,140 @@ - (void)setAlternateImage:(NSImage *)image
[self updateViewFrame];
}

////////////////////////////////////
#pragma mark - Helper
////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Notification Handler
/////////////////////////////////////////////////////////////////////////////////////

- (void)updateViewFrame
- (void)applicationDidResignActive:(NSNotification*)note
{
CGFloat width = MAX(MAX(kMinViewWidth, self.alternateImage.size.width), self.image.size.width);
CGFloat height = [NSStatusBar systemStatusBar].thickness;

NSRect frame = NSMakeRect(0, 0, width, height);
self.frame = frame;
_imageView.frame = frame;

[self setNeedsDisplay:YES];
[self hidePopover];
}

/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Popover Delegate
/////////////////////////////////////////////////////////////////////////////////////

//This is safer then caring for the sended events. Sometimes to popup doesn't close, in these
//cases popover and status item became out of sync
- (void)popoverWillShow: (NSNotification*) note
{
self.active = YES;
}

- (void)popoverWillClose: (NSNotification*) note
{
self.active = NO;
}

////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Show / Hide Popover
////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////

- (void)togglePopover
{
[self togglePopoverAnimated:self.isAnimated];
}

- (void)togglePopoverAnimated:(BOOL)animated
{
if (self.isActive) {
[self hidePopover];
} else {
[self showPopoverAnimated:animated];
}
}

- (void)showPopover
{
[self showPopoverAnimated:_animated];
[self showPopoverAnimated:self.isAnimated];
}

- (void)showPopoverAnimated:(BOOL)animated
{
self.active = YES;
BOOL willAnswer = [self.delegate respondsToSelector:@selector(shouldPopupOpen)];

if (!_popover) {
_popover = [[NSPopover alloc] init];
_popover.contentViewController = _viewController;
if (!willAnswer || (willAnswer && [self.delegate shouldPopupOpen])) {
if (!self.popover.isShown) {
_popover.animates = animated;
if ([self.delegate respondsToSelector:@selector(popupWillOpen)])
{
[self.delegate popupWillOpen];
}
[_popover showRelativeToRect:self.frame ofView:self preferredEdge:NSMinYEdge];
}

[self.window makeKeyWindow];

if ([self.delegate respondsToSelector:@selector(popupDidOpen)]) {
[self.delegate popupDidOpen];
}
}
}

- (void)hidePopover
{
BOOL willAnswer = [self.delegate respondsToSelector:@selector(shouldPopupClose)];

if (!_popover.isShown) {
_popover.animates = animated;
[_popover showRelativeToRect:self.frame ofView:self preferredEdge:NSMinYEdge];
_popoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:NSLeftMouseDownMask|NSRightMouseDownMask handler:^(NSEvent* event) {
[self hidePopover];
}];
if (!willAnswer || (willAnswer && [self.delegate shouldPopupClose])) {
if (_popover && _popover.isShown)
{
if ([self.delegate respondsToSelector:@selector(popupWillClose)]) {
[self.delegate popupWillClose];
}

[_popover close];
}

if ([self.delegate respondsToSelector:@selector(popupDidClose)]) {
[self.delegate popupDidClose];
}
}
}

- (void)hidePopover
/////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Helper
/////////////////////////////////////////////////////////////////////////////////////

- (void)updateViewFrame
{
self.active = NO;
CGFloat width = MAX(MAX(kMinViewWidth, self.alternateImage.size.width), self.image.size.width);
CGFloat height = [NSStatusBar systemStatusBar].thickness;

NSRect frame = NSMakeRect(0, 0, width, height);
self.frame = frame;
self.imageView.frame = frame;

if (_popover && _popover.isShown) {
[_popover close];
[NSEvent removeMonitor:_popoverTransiencyMonitor];
[self setNeedsDisplay:YES];
}

@end

///////////////////////////////////////////////////////////////////////////////////////////
#pragma mark - Implementation NSWindow+canBecomeKeyWindow
///////////////////////////////////////////////////////////////////////////////////////////

#import <objc/objc-class.h>

@implementation NSWindow (canBecomeKeyWindow)

//This is to fix a bug with 10.7 where an NSPopover with a text field
//cannot be edited if its parent window won't become key
//This technique is called method swizzling.
- (BOOL)swizzledPopoverCanBecomeKeyWindow
{
if (self == windowToOverride) {
return shouldBecomeKeyWindow;
} else {
return [self swizzledPopoverCanBecomeKeyWindow];
}
}

+ (void)load
{
method_exchangeImplementations(
class_getInstanceMethod(self, @selector(canBecomeKeyWindow)),
class_getInstanceMethod(self, @selector(swizzledPopoverCanBecomeKeyWindow)));
}

@end

Loading