Fix MusicVideo not showing non-first notifications on displays that don't have the menu bar on them.
The problem was that GrowlDisplayWindowController and GrowlPositionController were both trying to get the screen of a window that wasn't on a screen (yet|anymore), both before and after displaying the notification. The solution is to get the screen from the display window controller, not from the window.
2 // GrowlDisplayWindowController.m
5 // Created by Mac-arena the Bored Zo on 2005-06-03.
6 // Copyright 2004-2006 The Growl Project. All rights reserved.
9 #import "GrowlDisplayWindowController.h"
10 #import "GrowlPathUtilities.h"
11 #import "GrowlDefines.h"
12 #import "GrowlWindowTransition.h"
13 #import "GrowlPositionController.h"
14 #import "NSViewAdditions.h"
15 #import "GrowlNotificationDisplayBridge.h"
16 #import "GrowlApplicationNotification.h"
17 #import "GrowlNotificationView.h"
21 #define DEFAULT_TRANSITION_DURATION 0.75
23 static NSMutableDictionary *existingInstances;
25 @interface GrowlDisplayWindowController (PRIVATE)
26 - (void)cancelDisplayDelayedPerforms;
27 - (BOOL)supportsStickyNotifications;
30 @interface NSWindow (LeopardMethods)
31 - (void)setCollectionBehavior:(int)collectionBehavior;
34 @implementation GrowlDisplayWindowController
39 + (void) registerInstance:(id)instance withIdentifier:(NSString *)ident {
40 if (!existingInstances)
41 existingInstances = [[NSMutableDictionary alloc] init];
43 NSDictionary *classInstances = [existingInstances objectForKey:self];
44 if (!classInstances) {
45 classInstances = [[NSMutableDictionary alloc] init];
46 [existingInstances setObject:classInstances forKey:self];
47 [classInstances release];
49 [classInstances setValue:instance forKey:ident];
52 + (id) instanceWithIdentifier:(NSString *)ident {
53 NSMutableDictionary *classInstances = [existingInstances objectForKey:self];
55 return [classInstances objectForKey:ident];
60 + (void) unregisterInstanceWithIdentifier:(NSString *)ident {
61 NSMutableDictionary *classInstances = [existingInstances objectForKey:self];
63 [classInstances removeObjectForKey:ident];
68 - (id) initWithWindowNibName:(NSString *)windowNibName bridge:(GrowlNotificationDisplayBridge *)displayBridge {
69 // NOTE: for completeness we ought to offer the other nib related init methods with the plugin as a param
70 if ((self = [self initWithWindowNibName:windowNibName owner:displayBridge])) {
71 [self setBridge:displayBridge]; // weak reference
76 - (id) initWithBridge:(GrowlNotificationDisplayBridge *)displayBridge {
77 /* Subclasses using this method should call initWithWindowNibName: from init */
78 if ((self = [self init])) {
79 [self setBridge:displayBridge]; // weak reference
84 - (id) initWithWindow:(NSWindow *)window {
85 if ((self = [super initWithWindow:window])) {
86 windowTransitions = [[NSMutableDictionary alloc] init];
87 ignoresOtherNotifications = NO;
89 startTimes = NSCreateMapTable(NSObjectMapKeyCallBacks, NSIntMapValueCallBacks, 0U);
90 endTimes = NSCreateMapTable(NSObjectMapKeyCallBacks, NSIntMapValueCallBacks, 0U);
91 transitionDuration = DEFAULT_TRANSITION_DURATION;
93 //Show notifications on all Spaces
94 if ([window respondsToSelector:@selector(setCollectionBehavior:)]) {
95 #define NSWindowCollectionBehaviorCanJoinAllSpaces 1 << 0
96 [window setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
99 //Respond to 'close all notifications' by closing
100 [[NSNotificationCenter defaultCenter] addObserver:self
101 selector:@selector(stopDisplay)
102 name:GROWL_CLOSE_ALL_NOTIFICATIONS
110 [self setDelegate:nil];
111 [[self bridge] removeObserver:self forKeyPath:@"notification"];
112 [[NSNotificationCenter defaultCenter] removeObserver:self];
113 [self stopAllTransitions];
115 NSFreeMapTable(startTimes);
116 NSFreeMapTable(endTimes);
120 [clickContext release];
121 [clickHandlerEnabled release];
124 [windowTransitions release];
125 [notification release];
131 #pragma mark Screenshot mode
133 - (void) takeScreenshot {
134 NSView *view = [[self window] contentView];
135 NSString *path = [[[GrowlPathUtilities screenshotsDirectory] stringByAppendingPathComponent:[GrowlPathUtilities nextScreenshotName]] stringByAppendingPathExtension:@"png"];
136 [[view dataWithPNGInsideRect:[view frame]] writeToFile:path atomically:NO];
140 #pragma mark Display control
142 - (BOOL)reposition_startingDisplay:(BOOL)shouldStartDisplay
144 NSWindow *window = [self window];
146 //Make sure we don't cover any other notification (or not)
147 BOOL foundSpace = NO;
148 GrowlPositionController *pc = [GrowlPositionController sharedInstance];
149 if ([self respondsToSelector:@selector(idealOriginInRect:)])
150 foundSpace = [pc positionDisplay:self];
152 foundSpace = (ignoresOtherNotifications || [pc reserveRect:[window frame] forDisplayController:self]);
155 if (shouldStartDisplay) {
156 [self cancelDisplayDelayedPerforms];
158 [self willDisplayNotification];
160 [window orderFront:nil];
162 if ([self startAllTransitions]) {
163 [self performSelector:@selector(didFinishTransitionsBeforeDisplay)
165 afterDelay:transitionDuration];
167 [self didFinishTransitionsBeforeDisplay];
170 [self didDisplayNotification];
174 [[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerNotificationBlockedNotification
177 //Try again in 10 seconds
178 if (!shouldStartDisplay) {
179 //If we're restarting, get this display off-screen while we wait
180 //XXX This should be more fluid
181 [window orderOut:nil];
183 [[GrowlPositionController sharedInstance] clearReservedRectForDisplayController:self];
186 [self performSelector:@selector(startDisplay) withObject:nil afterDelay:5];
192 - (BOOL) startDisplay {
193 return [self reposition_startingDisplay:YES];
196 - (void) stopDisplay {
197 id contentView = [[self window] contentView];
198 if ([contentView respondsToSelector:@selector(mouseOver)] &&
199 [contentView mouseOver] &&
200 !userRequestedClose) {
201 //The mouse is currently within the view; close when it exits
202 [contentView setCloseOnMouseExit:YES];
205 //If we're already transitioning out, just keep doing our thing
206 if (displayStatus != GrowlDisplayTransitioningOutStatus) {
207 [self cancelDisplayDelayedPerforms];
209 [self willTakeDownNotification];
210 if ([self startAllTransitions]) {
211 [self performSelector:@selector(didFinishTransitionsAfterDisplay)
213 afterDelay:transitionDuration];
215 [self didFinishTransitionsAfterDisplay];
221 - (void) clickedClose {
222 userRequestedClose = YES;
227 #pragma mark Display stages
229 - (void)cancelDisplayDelayedPerforms
231 [[self class] cancelPreviousPerformRequestsWithTarget:self
232 selector:@selector(didFinishTransitionsBeforeDisplay)
235 [[self class] cancelPreviousPerformRequestsWithTarget:self
236 selector:@selector(didFinishTransitionsAfterDisplay)
239 [[self class] cancelPreviousPerformRequestsWithTarget:self
240 selector:@selector(stopDisplay)
244 - (void) willDisplayNotification {
245 displayStatus = GrowlDisplayTransitioningInStatus;
247 [[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerWillDisplayWindowNotification
251 - (void) didFinishTransitionsBeforeDisplay {
252 [self cancelDisplayDelayedPerforms];
254 if (![[[notification auxiliaryDictionary] objectForKey:GROWL_NOTIFICATION_STICKY] boolValue] ||
255 ![self supportsStickyNotifications]) {
256 [self performSelector:@selector(stopDisplay)
258 afterDelay:(displayDuration+transitionDuration)];
261 displayStatus = GrowlDisplayOnScreenStatus;
264 - (void) didFinishTransitionsAfterDisplay {
265 [self cancelDisplayDelayedPerforms];
267 //Clear the rect we reserved...
268 NSWindow *window = [self window];
269 [window orderOut:nil];
271 //Release all window transitions immediately; they may have retained our window.
272 [self stopAllTransitions];
273 [windowTransitions release]; windowTransitions = nil;
275 [[GrowlPositionController sharedInstance] clearReservedRectForDisplayController:self];
277 [self didTakeDownNotification];
279 if ((bridge) && ([bridge respondsToSelector:@selector(display)]))
280 [[bridge display] displayWindowControllerDidTakeDownWindow:self];
282 NSLog(@"%@ bridge does not respond to display",bridge);
286 - (void) didDisplayNotification {
288 [self takeScreenshot];
290 [[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerDidDisplayWindowNotification
294 - (void) willTakeDownNotification {
295 [[NSNotificationCenter defaultCenter] postNotificationName:GrowlDisplayWindowControllerWillTakeWindowDownNotification
297 displayStatus = GrowlDisplayTransitioningOutStatus;
300 - (void) didTakeDownNotification {
301 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
303 NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithCapacity:2U];
304 [userInfo setValue:clickContext forKey:GROWL_KEY_CLICKED_CONTEXT];
306 [userInfo setValue:appPid forKey:GROWL_APP_PID];
307 [nc postNotificationName:GROWL_NOTIFICATION_TIMED_OUT object:appName userInfo:userInfo];
310 //Avoid duplicate click messages by immediately clearing the clickContext
313 [nc postNotificationName:GrowlDisplayWindowControllerDidTakeWindowDownNotification object:self];
316 #pragma mark Click feedback
318 - (void) notificationClicked:(id)sender {
319 #pragma unused(sender)
321 NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] initWithCapacity:3U];
322 [userInfo setValue:clickHandlerEnabled forKey:@"ClickHandlerEnabled"];
323 [userInfo setValue:clickContext forKey:GROWL_KEY_CLICKED_CONTEXT];
325 [userInfo setValue:appPid forKey:GROWL_APP_PID];
326 [[NSNotificationCenter defaultCenter] postNotificationName:GROWL_NOTIFICATION_CLICKED
331 //Avoid duplicate click messages by immediately clearing the clickContext
335 if (target && action && [target respondsToSelector:action])
336 [target performSelector:action withObject:self];
338 //Now that we've notified the clickContext and target, it's as if the user just clicked the close button
343 #pragma mark Window Transitions
345 - (BOOL) addTransition:(GrowlWindowTransition *)transition {
346 [transition setWindow:[self window]];
347 [transition setDelegate:self];
348 if (![windowTransitions objectForKey:[transition class]]) {
349 [windowTransitions setObject:transition forKey:[transition class]];
355 - (void) removeTransition:(GrowlWindowTransition *)transition {
356 [transition setDelegate:nil];
357 [transition setWindow:nil];
359 [windowTransitions removeObjectForKey:[transition class]];
362 - (void) setStartPercentage:(unsigned)start endPercentage:(unsigned)end forTransition:(GrowlWindowTransition *)transition {
363 NSAssert1((start <= 100U || start < end),
364 @"The start parameter was invalid for the transition: %@",
366 NSAssert1((end <= 100U || start < end),
367 @"The end parameter was invalid for the transition: %@",
370 NSMapInsert(startTimes, transition, (void *)start);
371 NSMapInsert(endTimes, transition, (void *)end);
376 - (NSArray *) allTransitions {
377 return [windowTransitions allValues];
380 - (NSArray *) activeTransitions {
381 int count = [windowTransitions count];
382 NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
383 NSArray *transitionArray = [windowTransitions allValues];
386 for (i=0; i<count; ++i) {
387 GrowlWindowTransition *transition = [transitionArray objectAtIndex:i];
388 if ([transition isAnimating])
389 [result addObject:transition];
395 - (NSArray *) inactiveTransitions {
396 int count = [windowTransitions count];
397 NSMutableArray *result = [NSMutableArray arrayWithCapacity:count];
398 NSArray *transitionArray = [windowTransitions allValues];
401 for (i=0; i<count; ++i) {
402 GrowlWindowTransition *transition = [transitionArray objectAtIndex:i];
403 if (![transition isAnimating])
404 [result addObject:transition];
410 - (BOOL) startAllTransitions {
412 GrowlWindowTransition *transition;
413 NSEnumerator *transitionEnum = [[windowTransitions allValues] objectEnumerator];
415 while ((transition = [transitionEnum nextObject]))
416 if ([self startTransition:transition])
421 - (BOOL) startTransition:(GrowlWindowTransition *)transition {
422 int startPercentage = (int) NSMapGet(startTimes, transition);
423 int endPercentage = (int) NSMapGet(endTimes, transition);
425 // If there were no times set up then the end time would be NULL (0)...
426 if (endPercentage == 0)
429 // Work out the start and the end times...
430 CFTimeInterval startTime = (float)startPercentage * ((float)transitionDuration * 0.01);
431 CFTimeInterval endTime = (float)endPercentage * ((float)transitionDuration * 0.01);
433 // Set up this transition...
434 [transition setDuration: (endTime - startTime)];
435 [transition performSelector:@selector(startAnimation)
437 afterDelay:startTime];
442 - (BOOL) startTransitionOfKind:(Class)transitionClass {
443 GrowlWindowTransition *transition = [windowTransitions objectForKey:transitionClass];
445 return [self startTransition:transition];
449 - (void) stopAllTransitions {
450 GrowlWindowTransition *transition;
451 NSEnumerator *transitionEnum = [[windowTransitions allValues] objectEnumerator];
452 while (( transition = [transitionEnum nextObject] ))
453 [self stopTransition:transition];
456 - (void) stopTransition:(GrowlWindowTransition *)transition {
457 [transition stopAnimation];
458 [self removeTransition:transition];
460 [[self class] cancelPreviousPerformRequestsWithTarget:transition
461 selector:@selector(startAnimation)
465 - (void) stopTransitionOfKind:(Class)transitionClass {
466 GrowlWindowTransition *transition = [windowTransitions objectForKey:transitionClass];
468 [self stopTransition:transition];
471 - (void) animationDidEnd:(NSAnimation *)animation
473 if ([animation isKindOfClass:[GrowlWindowTransition class]] &&
474 ([(GrowlWindowTransition *)animation window] == [self window]) &&
475 ([(GrowlWindowTransition *)animation direction] == GrowlReverseTransition)) {
476 //A fade out nonrepeating animation finished. We don't need to wait on our timeout; we know we finished displaying a notification.
477 [self didFinishTransitionsAfterDisplay];
481 - (void) reverseAllTransitions
483 [[windowTransitions allValues] makeObjectsPerformSelector:@selector(reverse)];
487 - (void) mouseEnteredNotificationView:(GrowlNotificationView *)notificationView
489 #pragma unused (notificationView)
490 if (!userRequestedClose &&
491 (displayStatus == GrowlDisplayTransitioningOutStatus)) {
492 // We're transitioning out; we need to go back to transitioning in...
493 [self willDisplayNotification];
494 [self reverseAllTransitions];
495 [self didFinishTransitionsBeforeDisplay];
497 // ...but when the mouse leaves, transition out again
502 - (void) mouseExitedNotificationView:(GrowlNotificationView *)notificationView
504 #pragma unused (notificationView)
505 // Notifies us that the mouse left the notification view.
509 #pragma mark Notifications
511 - (GrowlApplicationNotification *) notification {
512 // Only here for binding conformance
516 - (void) setNotification:(GrowlApplicationNotification *)theNotification {
517 if (notification != theNotification) {
518 [notification release];
519 notification = [theNotification retain];
523 - (void) updateToNotification:(GrowlApplicationNotification *)theNotification {
524 [self setNotification:theNotification];
526 switch (displayStatus) {
527 case GrowlDisplayUnknownStatus:
528 case GrowlDisplayTransitioningInStatus:
529 //Do nothing; we're still transitioning in
532 case GrowlDisplayOnScreenStatus:
533 //We're on screen; reset our timer for transitioning out
534 [self didFinishTransitionsBeforeDisplay];
537 case GrowlDisplayTransitioningOutStatus:
538 //Reset userRequestedClose in case we were transitioning out via the user's request; we have new information!
539 userRequestedClose = NO;
541 [self willDisplayNotification];
542 [self reverseAllTransitions];
543 [self didFinishTransitionsBeforeDisplay];
548 [self reposition_startingDisplay:NO];
553 - (GrowlNotificationDisplayBridge *) bridge {
557 - (void) setBridge:(GrowlNotificationDisplayBridge *)theBridge {
558 if (bridge != theBridge) {
560 NSLog(@"*** This may be an error. %@ had its bridge reset", self);
561 [bridge removeObserver:self forKeyPath:@"notification"];
564 bridge = [theBridge retain];
566 [bridge addObserver:self forKeyPath:@"notification" options:NSKeyValueObservingOptionNew context:NULL];
567 [self observeValueForKeyPath:@"notification" ofObject:bridge change:nil context:NULL];
571 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
573 #pragma unused(change)
574 #pragma unused(context)
575 if ((object == bridge) &&
576 [keyPath isEqualToString:@"notification"]) {
577 [self setNotification:[bridge notification]];
583 - (CFTimeInterval) transitionDuration {
584 return transitionDuration;
587 - (void) setTransitionDuration:(CFTimeInterval)theTransitionDuration{
588 transitionDuration = theTransitionDuration;
593 - (CFTimeInterval) displayDuration {
594 return displayDuration;
597 - (void) setDisplayDuration:(CFTimeInterval)newDuration {
598 displayDuration = newDuration;
603 - (BOOL) screenshotModeEnabled {
604 return screenshotMode;
607 - (void) setScreenshotModeEnabled:(BOOL)newScreenshotMode {
608 screenshotMode = newScreenshotMode;
613 - (NSScreen *) screen {
614 NSArray *screens = [NSScreen screens];
615 if (screenNumber < [screens count])
616 return [screens objectAtIndex:screenNumber];
618 return [NSScreen mainScreen];
621 - (void) setScreen:(NSScreen *)newScreen {
622 unsigned newScreenNumber = [[NSScreen screens] indexOfObjectIdenticalTo:newScreen];
623 if (newScreenNumber == NSNotFound)
624 [NSException raise:NSInternalInconsistencyException format:@"Tried to set %@ %p to a screen %p that isn't in the screen list", [self class], self, newScreen];
625 [self setScreenNumber:newScreenNumber];
628 - (void) setScreenNumber:(unsigned)newScreenNumber {
629 [self willChangeValueForKey:@"screenNumber"];
630 screenNumber = newScreenNumber;
631 [self didChangeValueForKey:@"screenNumber"];
634 - (BOOL)supportsStickyNotifications
636 return ![[self window] ignoresMouseEvents];
645 - (void) setTarget:(id)object {
646 if (object != target) {
648 target = [object retain];
658 - (void) setAction:(SEL) selector {
664 - (NSString *) notifyingApplicationName {
668 - (void) setNotifyingApplicationName:(NSString *)inAppName {
669 if (inAppName != appName) {
671 appName = [inAppName copy];
677 - (NSNumber *) notifyingApplicationProcessIdentifier {
681 - (void) setNotifyingApplicationProcessIdentifier:(NSNumber *)inAppPid {
682 if (inAppPid != appPid) {
684 appPid = [inAppPid retain];
690 - (id) clickContext {
694 - (void) setClickContext:(id)inClickContext {
695 if (clickContext != inClickContext) {
696 [clickContext release];
697 clickContext = [inClickContext retain];
703 - (BOOL) ignoresOtherNotifications {
704 return ignoresOtherNotifications;
707 - (void) setIgnoresOtherNotifications:(BOOL)flag {
708 ignoresOtherNotifications = flag;
717 - (void) setDelegate:(id)newDelegate {
719 [self removeNotificationObserver:delegate];
722 [self addNotificationObserver:newDelegate];
724 delegate = newDelegate;
729 - (NSNumber *) clickHandlerEnabled {
730 return clickHandlerEnabled;
733 - (void) setClickHandlerEnabled:(NSNumber *)flag {
734 if (flag != clickHandlerEnabled) {
735 [clickHandlerEnabled release];
736 clickHandlerEnabled = [flag retain];
742 - (void) addNotificationObserver:(id)observer {
743 NSParameterAssert(observer != nil);
745 NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
748 //register the new delegate.
749 if ([observer respondsToSelector:@selector(displayWindowControllerWillDisplayWindow:)])
750 [nc addObserver:observer
751 selector:@selector(displayWindowControllerWillDisplayWindow:)
752 name:GrowlDisplayWindowControllerWillDisplayWindowNotification
754 if ([observer respondsToSelector:@selector(displayWindowControllerDidDisplayWindow:)])
755 [nc addObserver:observer
756 selector:@selector(displayWindowControllerDidDisplayWindow:)
757 name:GrowlDisplayWindowControllerDidDisplayWindowNotification
760 if ([observer respondsToSelector:@selector(displayWindowControllerWillTakeDownWindow:)])
761 [nc addObserver:observer
762 selector:@selector(displayWindowControllerWillTakeWindowDown:)
763 name:GrowlDisplayWindowControllerWillTakeWindowDownNotification
765 if ([observer respondsToSelector:@selector(displayWindowControllerDidTakeWindowDown:)])
766 [nc addObserver:observer
767 selector:@selector(displayWindowControllerDidTakeWindowDown:)
768 name:GrowlDisplayWindowControllerDidTakeWindowDownNotification
770 if ([observer respondsToSelector:@selector(displayWindowControllerNotificationBlocked:)])
771 [nc addObserver:observer
772 selector:@selector(displayWindowControllerNotificationBlocked:)
773 name:GrowlDisplayWindowControllerNotificationBlockedNotification
777 - (void) removeNotificationObserver:(id)observer {
778 [[NSNotificationCenter defaultCenter] removeObserver:observer];