CFRelease the duration preference value if we get one. Found by the clang static analyzer.
2 // GrowlWebKitWindowController.m
5 // Created by Ingmar Stein on Thu Apr 14 2005.
6 // Copyright 2005-2006 The Growl Project. All rights reserved.
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"
27 * A panel that always pretends to be the key window.
29 @interface KeyPanel : NSPanel {
33 @implementation KeyPanel
34 - (BOOL) isKeyWindow {
39 @interface NSData (Base64Additions)
40 - (NSString *)base64Encoding;
43 @interface NSImage (PNGRepAddition)
44 - (NSData *)PNGRepresentation;
47 @implementation GrowlWebKitWindowController
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
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
62 if (!(self = [super initWithWindow:panel]))
65 GrowlDisplayPlugin *plugin = [displayBridge display];
67 // Read the template file....exit on error...
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
77 NSLog(@"ERROR: could not read template '%@' - %@", templateFile,error);
81 baseURL = [[NSURL fileURLWithPath:[displayBundle resourcePath]] retain];
83 // Read the prefs for the plugin...
84 unsigned theScreenNo = 0U;
85 READ_GROWL_PREF_INT(GrowlWebKitScreenPref, [plugin prefDomain], &theScreenNo);
86 [self setScreenNumber:theScreenNo];
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);
95 // Read the plugin specifics from the info.plist
96 NSDictionary *styleInfo = [[plugin bundle] infoDictionary];
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"];
104 paddingX = [xPad floatValue];
106 paddingY = [yPad floatValue];
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];
123 // Configure the view
124 NSRect panelFrame = [panel frame];
125 GrowlWebKitWindowView *view = [[GrowlWebKitWindowView alloc] initWithFrame:panelFrame
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]];
139 [self setBridge:displayBridge];
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];
154 GrowlWebKitWindowView *webView = [[self window] contentView];
155 [webView setPolicyDelegate:nil];
156 [webView setFrameLoadDelegate:nil];
157 [webView setTarget:nil];
159 [templateHTML release];
165 - (void) setTitle:(NSString *)title text:(NSString *)text icon:(NSImage *)icon priority:(int)priority forView:(WebView *)view {
166 CFStringRef priorityName;
169 priorityName = CFSTR("verylow");
172 priorityName = CFSTR("moderate");
176 priorityName = CFSTR("normal");
179 priorityName = CFSTR("high");
182 priorityName = CFSTR("emergency");
186 CFMutableStringRef htmlString = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, (CFStringRef)templateHTML);
188 NSString *imageMediaType = @"image/png";
189 NSData *imageData = [icon PNGRepresentation];
191 //Couldn't create a PNG, so fall back on TIFF.
192 imageMediaType = @"image/tiff";
193 imageData = [icon TIFFRepresentation];
195 NSString *growlImageString = [NSString stringWithFormat:@"data:%@;base64,%@", imageMediaType, [imageData base64Encoding]];
197 float opacity = 95.0f;
198 READ_GROWL_PREF_FLOAT(GrowlWebKitOpacityPref, [[bridge display] prefDomain], &opacity);
201 CFStringRef titleHTML = createStringByEscapingForHTML((CFStringRef)title);
202 CFStringRef textHTML = createStringByEscapingForHTML((CFStringRef)text);
203 CFStringRef opacityString = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%f"), opacity);
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);
212 CFRelease(opacityString);
213 CFRelease(titleHTML);
215 WebFrame *webFrame = [view mainFrame];
216 [[self window] disableFlushWindow];
218 [webFrame loadHTMLString:(NSString *)htmlString baseURL:nil];
219 [[webFrame frameView] setAllowsScrolling:NO];
220 CFRelease(htmlString);
224 * @brief Prevent the webview from following external links. We direct these to the users web browser.
226 - (void) webView:(WebView *)sender
227 decidePolicyForNavigationAction:(NSDictionary *)actionInformation
228 request:(NSURLRequest *)request
229 frame:(WebFrame *)frame
230 decisionListener:(id<WebPolicyDecisionListener>)listener
232 #pragma unused(sender, request, frame)
233 int actionKey = getIntegerForKey(actionInformation, WebActionNavigationTypeKey);
234 if (actionKey == WebNavigationTypeOther) {
237 NSURL *url = getObjectForKey(actionInformation, WebActionOriginalURLKey);
239 //Ignore file URLs, but open anything else
240 if (![url isFileURL])
241 [[NSWorkspace sharedWorkspace] openURL:url];
248 * @brief Invoked once the webview has loaded and is ready to accept content
250 - (void) webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
251 #pragma unused(frame)
252 NSWindow *myWindow = [self window];
253 if ([myWindow isFlushWindowDisabled])
254 [myWindow enableFlushWindow];
256 GrowlWebKitWindowView *view = (GrowlWebKitWindowView *)sender;
259 //Update our new frame
260 [[GrowlPositionController sharedInstance] positionDisplay:self];
262 [myWindow invalidateShadow];
265 - (void) setNotification:(GrowlApplicationNotification *)theNotification {
266 if (notification == theNotification)
269 [super setNotification:theNotification];
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);
278 NSPanel *panel = (NSPanel *)[self window];
279 WebView *view = [panel contentView];
280 [self setTitle:title text:text icon:icon priority:priority forView:view];
282 // NSRect panelFrame = [view frame];
284 // [panel setFrame:panelFrame display:NO];
288 #pragma mark positioning methods
290 - (NSPoint) idealOriginInRect:(NSRect)rect {
291 NSRect viewFrame = [[[self window] contentView] frame];
292 enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
295 switch(originatingPosition){
296 case GrowlTopRightPosition:
297 idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
298 NSMaxY(rect) - paddingY - NSHeight(viewFrame));
300 case GrowlTopLeftPosition:
301 idealOrigin = NSMakePoint(NSMinX(rect) + paddingX,
302 NSMaxY(rect) - paddingY - NSHeight(viewFrame));
304 case GrowlBottomLeftPosition:
305 idealOrigin = NSMakePoint(NSMinX(rect) + paddingX,
306 NSMinY(rect) + paddingY);
308 case GrowlBottomRightPosition:
309 idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
310 NSMinY(rect) + paddingY);
313 idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
314 NSMaxY(rect) - paddingY - NSHeight(viewFrame));
321 - (enum GrowlExpansionDirection) primaryExpansionDirection {
322 enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
323 enum GrowlExpansionDirection directionToExpand;
325 switch(originatingPosition){
326 case GrowlTopLeftPosition:
327 directionToExpand = GrowlDownExpansionDirection;
329 case GrowlTopRightPosition:
330 directionToExpand = GrowlDownExpansionDirection;
332 case GrowlBottomLeftPosition:
333 directionToExpand = GrowlUpExpansionDirection;
335 case GrowlBottomRightPosition:
336 directionToExpand = GrowlUpExpansionDirection;
339 directionToExpand = GrowlDownExpansionDirection;
343 return directionToExpand;
346 - (enum GrowlExpansionDirection) secondaryExpansionDirection {
347 enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
348 enum GrowlExpansionDirection directionToExpand;
350 switch(originatingPosition){
351 case GrowlTopLeftPosition:
352 directionToExpand = GrowlRightExpansionDirection;
354 case GrowlTopRightPosition:
355 directionToExpand = GrowlLeftExpansionDirection;
357 case GrowlBottomLeftPosition:
358 directionToExpand = GrowlRightExpansionDirection;
360 case GrowlBottomRightPosition:
361 directionToExpand = GrowlLeftExpansionDirection;
364 directionToExpand = GrowlRightExpansionDirection;
368 return directionToExpand;
371 - (float) requiredDistanceFromExistingDisplays {
377 @implementation NSData (Base64Additions)
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','+','/' };
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;
397 ctremaining = lentext - ixtext;
398 if( ctremaining <= 0 ) break;
400 for( i = 0; i < 3; i++ ) {
402 if( ix < lentext ) inbuf[i] = bytes[ix];
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;
412 switch( ctremaining ) {
421 for( i = 0; i < ctcopy; i++ )
422 [result appendFormat:@"%c", encodingTable[outbuf[i]]];
424 for( i = ctcopy; i < 4; i++ )
425 [result appendString:@"="];
430 if( lineLength > 0 ) {
431 if( charsonline >= lineLength ) {
433 [result appendString:@"\n"];
441 - (NSString *) base64Encoding {
442 return [self base64EncodingWithLineLength:0];
447 @implementation NSImage (PNGRepAddition)
448 - (NSBitmapImageRep *)GrowlBitmapImageRepForPNG
450 //Find the biggest image
451 NSEnumerator *repsEnum = [[self representations] objectEnumerator];
452 NSBitmapImageRep *bestRep = nil;
454 Class NSBitmapImageRepClass = [NSBitmapImageRep class];
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;
474 - (NSData *)PNGRepresentation
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]);