Plugins/Displays/WebKit/GrowlWebKitWindowController.m
author Peter Hosey
Fri May 22 19:33:14 2009 -0700 (2009-05-22)
branchtransition-experiments
changeset 4221 fee030c66025
parent 4167 fbb86d40b0af
child 4246 4f52d1d98978
permissions -rw-r--r--
Increase the default duration of all visual displays (except MusicVideo) to compensate for the faster transition time.
     1 //
     2 //  GrowlWebKitWindowController.m
     3 //  Growl
     4 //
     5 //  Created by Ingmar Stein on Thu Apr 14 2005.
     6 //  Copyright 2005-2006 The Growl Project. All rights reserved.
     7 //
     8 
     9 #import "GrowlWebKitWindowController.h"
    10 #import "GrowlWebKitWindowView.h"
    11 #import "GrowlWebKitPrefsController.h"
    12 #import "GrowlWebKitDefines.h"
    13 #import "NSWindow+Transforms.h"
    14 #import "GrowlPluginController.h"
    15 #import "NSViewAdditions.h"
    16 #import "GrowlDefines.h"
    17 #import "GrowlPathUtilities.h"
    18 #import "GrowlApplicationNotification.h"
    19 #include "CFGrowlAdditions.h"
    20 #include "CFDictionaryAdditions.h"
    21 #include "CFMutableStringAdditions.h"
    22 #import "GrowlNotificationDisplayBridge.h"
    23 #import "GrowlDisplayPlugin.h"
    24 #import "GrowlFadingWindowTransition.h"
    25 
    26 /*
    27  * A panel that always pretends to be the key window.
    28  */
    29 @interface KeyPanel : NSPanel {
    30 }
    31 @end
    32 
    33 @implementation KeyPanel
    34 - (BOOL) isKeyWindow {
    35 	return YES;
    36 }
    37 @end
    38 
    39 @interface NSData (Base64Additions)
    40 - (NSString *)base64Encoding;
    41 @end
    42 
    43 @interface NSImage (PNGRepAddition)
    44 - (NSData *)PNGRepresentation;
    45 @end
    46 
    47 @implementation GrowlWebKitWindowController
    48 
    49 #define GrowlWebKitDurationPrefDefault				5.0
    50 #define ADDITIONAL_LINES_DISPLAY_TIME	0.5
    51 #define MAX_DISPLAY_TIME				10.0
    52 #define GrowlWebKitPadding				5.0f
    53 
    54 #pragma mark -
    55 
    56 - (id) initWithBridge:(GrowlNotificationDisplayBridge *)displayBridge {
    57 	// init the window used to init
    58 	NSPanel *panel = [[KeyPanel alloc] initWithContentRect:NSMakeRect(0.0f, 0.0f, 270.0f, 1.0f)
    59 												 styleMask:NSBorderlessWindowMask | NSNonactivatingPanelMask
    60 												   backing:NSBackingStoreBuffered
    61 													 defer:YES];
    62 	if (!(self = [super initWithWindow:panel])) {
    63 		[panel release];
    64 		return nil;
    65 	}
    66 
    67 	GrowlDisplayPlugin *plugin = [displayBridge display];
    68 
    69 	// Read the template file....exit on error...
    70 	NSError *error = nil;
    71 	NSBundle *displayBundle = [plugin bundle];
    72 	NSString *templateFile = [displayBundle pathForResource:@"template" ofType:@"html"];
    73 	if (![[NSFileManager defaultManager] fileExistsAtPath:templateFile])
    74 		templateFile = [[NSBundle mainBundle] pathForResource:@"template" ofType:@"html"];
    75 	templateHTML = [[NSString alloc] initWithContentsOfFile:templateFile
    76 												   encoding:NSUTF8StringEncoding
    77 													  error:&error];
    78 	if (!templateHTML) {
    79 		NSLog(@"ERROR: could not read template '%@' - %@", templateFile,error);
    80 		[self release];
    81 		return nil;
    82 	}
    83 	baseURL = [[NSURL fileURLWithPath:[displayBundle resourcePath]] retain];
    84 
    85 	// Read the prefs for the plugin...
    86 	unsigned theScreenNo = 0U;
    87 	READ_GROWL_PREF_INT(GrowlWebKitScreenPref, [plugin prefDomain], &theScreenNo);
    88 	[self setScreenNumber:theScreenNo];
    89 
    90 	CFNumberRef prefsDuration = NULL;
    91 	READ_GROWL_PREF_VALUE(GrowlWebKitDurationPref, [plugin prefDomain], CFNumberRef, &prefsDuration);
    92 	[self setDisplayDuration:(prefsDuration ?
    93 							  [(NSNumber *)prefsDuration doubleValue] :
    94 							  GrowlWebKitDurationPrefDefault)];
    95 	if (prefsDuration) CFRelease(prefsDuration);
    96 	
    97 	// Read the plugin specifics from the info.plist
    98 	NSDictionary *styleInfo = [[plugin bundle] infoDictionary];
    99 	BOOL hasShadow = NO;
   100 	hasShadow =	[(NSNumber *)[styleInfo valueForKey:@"GrowlHasShadow"] boolValue];
   101 	paddingX = GrowlWebKitPadding;
   102 	paddingY = GrowlWebKitPadding;
   103 	NSNumber *xPad = [styleInfo valueForKey:@"GrowlPaddingX"];
   104 	NSNumber *yPad = [styleInfo valueForKey:@"GrowlPaddingY"];
   105 	if (xPad)
   106 		paddingX = [xPad floatValue];
   107 	if (yPad)
   108 		paddingY = [yPad floatValue];
   109 
   110 	// Configure the window
   111 	[panel setBecomesKeyOnlyIfNeeded:YES];
   112 	[panel setHidesOnDeactivate:NO];
   113 	[panel setBackgroundColor:[NSColor clearColor]];
   114 	[panel setLevel:NSStatusWindowLevel];
   115 	[panel setSticky:YES];
   116 	[panel setAlphaValue:0.0f];
   117 	[panel setOpaque:NO];
   118 	[panel setCanHide:NO];
   119 	[panel setOneShot:YES];
   120 	[panel useOptimizedDrawing:YES];
   121 	[panel disableCursorRects];
   122 	[panel setHasShadow:hasShadow];
   123 	[panel setDelegate:self];
   124 
   125 	// Configure the view
   126 	NSRect panelFrame = [panel frame];
   127 	GrowlWebKitWindowView *view = [[GrowlWebKitWindowView alloc] initWithFrame:panelFrame
   128 																	 frameName:nil
   129 																	 groupName:nil];
   130 	[view setMaintainsBackForwardList:NO];
   131 	[view setTarget:self];
   132 	[view setAction:@selector(notificationClicked:)];
   133 	[view setPolicyDelegate:self];
   134 	[view setFrameLoadDelegate:self];
   135 	if ([view respondsToSelector:@selector(setDrawsBackground:)])
   136 		[view setDrawsBackground:NO];
   137 	[panel setContentView:view];
   138 	[panel makeFirstResponder:[[[view mainFrame] frameView] documentView]];
   139 	[view release];
   140 
   141 	[self setBridge:displayBridge];
   142 
   143 	// set up the transitions...
   144 	GrowlFadingWindowTransition *fader = [[GrowlFadingWindowTransition alloc] initWithWindow:panel];
   145 	[self addTransition:fader];
   146 	[self setStartPercentage:0 endPercentage:100 forTransition:fader];
   147 	[fader setAutoReverses:YES];
   148 	[fader release];
   149 
   150 	[panel release];
   151 
   152 	return self;
   153 }
   154 
   155 - (void) dealloc {
   156 	GrowlWebKitWindowView *webView = [[self window] contentView];
   157 	[webView      setPolicyDelegate:nil];
   158 	[webView      setFrameLoadDelegate:nil];
   159 	[webView      setTarget:nil];
   160 
   161 	[templateHTML release];
   162 	[baseURL	  release];
   163 	
   164 	[super dealloc];
   165 }
   166 
   167 - (void) setTitle:(NSString *)title text:(NSString *)text icon:(NSImage *)icon priority:(int)priority forView:(WebView *)view {
   168 	CFStringRef priorityName;
   169 	switch (priority) {
   170 		case -2:
   171 			priorityName = CFSTR("verylow");
   172 			break;
   173 		case -1:
   174 			priorityName = CFSTR("moderate");
   175 			break;
   176 		default:
   177 		case 0:
   178 			priorityName = CFSTR("normal");
   179 			break;
   180 		case 1:
   181 			priorityName = CFSTR("high");
   182 			break;
   183 		case 2:
   184 			priorityName = CFSTR("emergency");
   185 			break;
   186 	}
   187 
   188 	CFMutableStringRef htmlString = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, (CFStringRef)templateHTML);
   189 
   190 	NSString *imageMediaType = @"image/png";
   191 	NSData *imageData = [icon PNGRepresentation];
   192 	if (!imageData) {
   193 		//Couldn't create a PNG, so fall back on TIFF.
   194 		imageMediaType = @"image/tiff";
   195 		imageData = [icon TIFFRepresentation];
   196 	}
   197 	NSString *growlImageString = [NSString stringWithFormat:@"data:%@;base64,%@", imageMediaType, [imageData base64Encoding]];
   198 
   199 	float opacity = 95.0f;
   200 	READ_GROWL_PREF_FLOAT(GrowlWebKitOpacityPref, [[bridge display] prefDomain], &opacity);
   201 	opacity *= 0.01f;
   202 
   203 	CFStringRef titleHTML = createStringByEscapingForHTML((CFStringRef)title);
   204 	CFStringRef textHTML = createStringByEscapingForHTML((CFStringRef)text);
   205 	CFStringRef opacityString = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%f"), opacity);
   206 
   207 	CFStringFindAndReplace(htmlString, CFSTR("%baseurl%"),  (CFStringRef)[baseURL absoluteString], CFRangeMake(0, CFStringGetLength(htmlString)), 0);
   208 	CFStringFindAndReplace(htmlString, CFSTR("%opacity%"),  opacityString,				 CFRangeMake(0, CFStringGetLength(htmlString)), 0);
   209 	CFStringFindAndReplace(htmlString, CFSTR("%priority%"), priorityName,				 CFRangeMake(0, CFStringGetLength(htmlString)), 0);
   210 	CFStringFindAndReplace(htmlString, CFSTR("growlimage://%image%"), (CFStringRef)growlImageString, CFRangeMake(0, CFStringGetLength(htmlString)), 0);
   211 	CFStringFindAndReplace(htmlString, CFSTR("%title%"),    (CFStringRef)titleHTML,		 CFRangeMake(0, CFStringGetLength(htmlString)), 0);
   212 	CFStringFindAndReplace(htmlString, CFSTR("%text%"),     (CFStringRef)textHTML,		 CFRangeMake(0, CFStringGetLength(htmlString)), 0);
   213 
   214 	CFRelease(opacityString);
   215 	CFRelease(titleHTML);
   216 	CFRelease(textHTML);
   217 	WebFrame *webFrame = [view mainFrame];
   218 	[[self window] disableFlushWindow];
   219 
   220 	[webFrame loadHTMLString:(NSString *)htmlString baseURL:nil];
   221 	[[webFrame frameView] setAllowsScrolling:NO];
   222 	CFRelease(htmlString);
   223 }
   224 
   225 /*!
   226  * @brief Prevent the webview from following external links.  We direct these to the users web browser.
   227  */
   228 - (void) webView:(WebView *)sender
   229 	decidePolicyForNavigationAction:(NSDictionary *)actionInformation
   230 		request:(NSURLRequest *)request
   231 		  frame:(WebFrame *)frame
   232 	decisionListener:(id<WebPolicyDecisionListener>)listener
   233 {
   234 #pragma unused(sender, request, frame)
   235 	int actionKey = getIntegerForKey(actionInformation, WebActionNavigationTypeKey);
   236 	if (actionKey == WebNavigationTypeOther) {
   237 		[listener use];
   238 	} else {
   239 		NSURL *url = getObjectForKey(actionInformation, WebActionOriginalURLKey);
   240 
   241 		//Ignore file URLs, but open anything else
   242 		if (![url isFileURL])
   243 			[[NSWorkspace sharedWorkspace] openURL:url];
   244 
   245 		[listener ignore];
   246 	}
   247 }
   248 
   249 /*!
   250  * @brief Invoked once the webview has loaded and is ready to accept content
   251  */
   252 - (void) webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
   253 #pragma unused(frame)
   254 	NSWindow *myWindow = [self window];
   255 	if ([myWindow isFlushWindowDisabled])
   256 		[myWindow enableFlushWindow];
   257 
   258 	GrowlWebKitWindowView *view = (GrowlWebKitWindowView *)sender;
   259 	[view sizeToFit];
   260 
   261 	//Update our new frame
   262 	[[GrowlPositionController sharedInstance] positionDisplay:self];
   263 
   264 	[myWindow invalidateShadow];
   265 }
   266 
   267 - (void) setNotification:(GrowlApplicationNotification *)theNotification {
   268     if (notification == theNotification)
   269 		return;
   270 
   271 	[super setNotification:theNotification];
   272 
   273 	// Extract the new details from the notification
   274 	NSDictionary *noteDict = [notification dictionaryRepresentation];
   275 	NSString *title = [notification title];
   276 	NSString *text  = [notification notificationDescription];
   277 	NSImage *icon   = getObjectForKey(noteDict, GROWL_NOTIFICATION_ICON);
   278 	int priority    = getIntegerForKey(noteDict, GROWL_NOTIFICATION_PRIORITY);
   279 
   280 	NSPanel *panel = (NSPanel *)[self window];
   281 	WebView *view = [panel contentView];
   282 	[self setTitle:title text:text icon:icon priority:priority forView:view];
   283 
   284 //	NSRect panelFrame = [view frame];
   285 	
   286 //	[panel setFrame:panelFrame display:NO];
   287 }
   288 
   289 #pragma mark -
   290 #pragma mark positioning methods
   291 
   292 - (NSPoint) idealOriginInRect:(NSRect)rect {
   293 	NSRect viewFrame = [[[self window] contentView] frame];
   294 	enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
   295 	NSPoint idealOrigin;
   296 
   297 	switch(originatingPosition){
   298 		case GrowlTopRightPosition:
   299 			idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
   300 									  NSMaxY(rect) - paddingY - NSHeight(viewFrame));
   301 			break;
   302 		case GrowlTopLeftPosition:
   303 			idealOrigin = NSMakePoint(NSMinX(rect) + paddingX,
   304 									  NSMaxY(rect) - paddingY - NSHeight(viewFrame));
   305 			break;
   306 		case GrowlBottomLeftPosition:
   307 			idealOrigin = NSMakePoint(NSMinX(rect) + paddingX,
   308 									  NSMinY(rect) + paddingY);
   309 			break;
   310 		case GrowlBottomRightPosition:
   311 			idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
   312 									  NSMinY(rect) + paddingY);
   313 			break;
   314 		default:
   315 			idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
   316 									  NSMaxY(rect) - paddingY - NSHeight(viewFrame));
   317 			break;			
   318 	}
   319 
   320 	return idealOrigin;	
   321 }
   322 
   323 - (enum GrowlExpansionDirection) primaryExpansionDirection {
   324 	enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
   325 	enum GrowlExpansionDirection directionToExpand;
   326 	
   327 	switch(originatingPosition){
   328 		case GrowlTopLeftPosition:
   329 			directionToExpand = GrowlDownExpansionDirection;
   330 			break;
   331 		case GrowlTopRightPosition:
   332 			directionToExpand = GrowlDownExpansionDirection;
   333 			break;
   334 		case GrowlBottomLeftPosition:
   335 			directionToExpand = GrowlUpExpansionDirection;
   336 			break;
   337 		case GrowlBottomRightPosition:
   338 			directionToExpand = GrowlUpExpansionDirection;
   339 			break;
   340 		default:
   341 			directionToExpand = GrowlDownExpansionDirection;
   342 			break;			
   343 	}
   344 	
   345 	return directionToExpand;
   346 }
   347 
   348 - (enum GrowlExpansionDirection) secondaryExpansionDirection {
   349 	enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
   350 	enum GrowlExpansionDirection directionToExpand;
   351 	
   352 	switch(originatingPosition){
   353 		case GrowlTopLeftPosition:
   354 			directionToExpand = GrowlRightExpansionDirection;
   355 			break;
   356 		case GrowlTopRightPosition:
   357 			directionToExpand = GrowlLeftExpansionDirection;
   358 			break;
   359 		case GrowlBottomLeftPosition:
   360 			directionToExpand = GrowlRightExpansionDirection;
   361 			break;
   362 		case GrowlBottomRightPosition:
   363 			directionToExpand = GrowlLeftExpansionDirection;
   364 			break;
   365 		default:
   366 			directionToExpand = GrowlRightExpansionDirection;
   367 			break;
   368 	}
   369 	
   370 	return directionToExpand;
   371 }
   372 
   373 - (float) requiredDistanceFromExistingDisplays {
   374 	return paddingY;
   375 }
   376 
   377 @end
   378 
   379 @implementation NSData (Base64Additions)
   380 
   381 static char encodingTable[64] = {
   382 	'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
   383 	'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
   384 	'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
   385 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/' };
   386 
   387 - (NSString *) base64EncodingWithLineLength:(unsigned int) lineLength {
   388 	const unsigned char	*bytes = [self bytes];
   389 	NSMutableString *result = [NSMutableString stringWithCapacity:[self length]];
   390 	unsigned long ixtext = 0;
   391 	unsigned long lentext = [self length];
   392 	long ctremaining = 0;
   393 	unsigned char inbuf[3], outbuf[4];
   394 	unsigned short i = 0;
   395 	unsigned short charsonline = 0, ctcopy = 0;
   396 	unsigned long ix = 0;
   397 	
   398 	while( YES ) {
   399 		ctremaining = lentext - ixtext;
   400 		if( ctremaining <= 0 ) break;
   401 		
   402 		for( i = 0; i < 3; i++ ) {
   403 			ix = ixtext + i;
   404 			if( ix < lentext ) inbuf[i] = bytes[ix];
   405 			else inbuf [i] = 0;
   406 		}
   407 		
   408 		outbuf [0] = (inbuf [0] & 0xFC) >> 2;
   409 		outbuf [1] = ((inbuf [0] & 0x03) << 4) | ((inbuf [1] & 0xF0) >> 4);
   410 		outbuf [2] = ((inbuf [1] & 0x0F) << 2) | ((inbuf [2] & 0xC0) >> 6);
   411 		outbuf [3] = inbuf [2] & 0x3F;
   412 		ctcopy = 4;
   413 		
   414 		switch( ctremaining ) {
   415 			case 1:
   416 				ctcopy = 2;
   417 				break;
   418 			case 2:
   419 				ctcopy = 3;
   420 				break;
   421 		}
   422 		
   423 		for( i = 0; i < ctcopy; i++ )
   424 			[result appendFormat:@"%c", encodingTable[outbuf[i]]];
   425 		
   426 		for( i = ctcopy; i < 4; i++ )
   427 			[result appendString:@"="];
   428 		
   429 		ixtext += 3;
   430 		charsonline += 4;
   431 		
   432 		if( lineLength > 0 ) {
   433 			if( charsonline >= lineLength ) {
   434 				charsonline = 0;
   435 				[result appendString:@"\n"];
   436 			}
   437 		}
   438 	}
   439 	
   440 	return result;
   441 }
   442 
   443 - (NSString *) base64Encoding {
   444 	return [self base64EncodingWithLineLength:0];
   445 }
   446 
   447 @end
   448 
   449 @implementation NSImage (PNGRepAddition)
   450 - (NSBitmapImageRep *)GrowlBitmapImageRepForPNG
   451 {
   452 	//Find the biggest image
   453 	NSEnumerator *repsEnum = [[self representations] objectEnumerator];
   454 	NSBitmapImageRep *bestRep = nil;
   455 	NSImageRep *rep;
   456 	Class NSBitmapImageRepClass = [NSBitmapImageRep class];
   457 	float maxWidth = 0;
   458 	while ((rep = [repsEnum nextObject])) {
   459 		if ([rep isKindOfClass:NSBitmapImageRepClass]) {
   460 			//We can't convert a 1-bit image to PNG format (libpng throws an error), so ignore any 1-bit image reps, regardless of size.
   461 			if ([rep bitsPerSample] > 1) {
   462 				float width = [rep size].width;
   463 				if (width >= maxWidth) {
   464 					//Cast explanation: GCC warns about us returning an NSImageRep here, presumably because it could be some other kind of NSImageRep if we don't check the class. Fortunately, we have such a check. This cast silences the warning.
   465 					bestRep = (NSBitmapImageRep *)rep;
   466 
   467 					maxWidth = width;
   468 				}
   469 			}
   470 		}
   471 	}
   472 	
   473 	return bestRep;
   474 }
   475 
   476 - (NSData *)PNGRepresentation
   477 {
   478 	/* PNG is easy; it supports almost everything TIFF does (not 1-bit images), and NSImage's PNG support is great. */
   479 	return ([(NSBitmapImageRep *)[self GrowlBitmapImageRepForPNG] representationUsingType:NSPNGFileType properties:nil]);
   480 }
   481 
   482 @end