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