Increase the default duration of all visual displays (except MusicVideo) to compensate for the faster transition time.
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 5.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])) {
67 GrowlDisplayPlugin *plugin = [displayBridge display];
69 // Read the template file....exit on error...
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
79 NSLog(@"ERROR: could not read template '%@' - %@", templateFile,error);
83 baseURL = [[NSURL fileURLWithPath:[displayBundle resourcePath]] retain];
85 // Read the prefs for the plugin...
86 unsigned theScreenNo = 0U;
87 READ_GROWL_PREF_INT(GrowlWebKitScreenPref, [plugin prefDomain], &theScreenNo);
88 [self setScreenNumber:theScreenNo];
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);
97 // Read the plugin specifics from the info.plist
98 NSDictionary *styleInfo = [[plugin bundle] infoDictionary];
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"];
106 paddingX = [xPad floatValue];
108 paddingY = [yPad floatValue];
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];
125 // Configure the view
126 NSRect panelFrame = [panel frame];
127 GrowlWebKitWindowView *view = [[GrowlWebKitWindowView alloc] initWithFrame:panelFrame
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]];
141 [self setBridge:displayBridge];
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];
156 GrowlWebKitWindowView *webView = [[self window] contentView];
157 [webView setPolicyDelegate:nil];
158 [webView setFrameLoadDelegate:nil];
159 [webView setTarget:nil];
161 [templateHTML release];
167 - (void) setTitle:(NSString *)title text:(NSString *)text icon:(NSImage *)icon priority:(int)priority forView:(WebView *)view {
168 CFStringRef priorityName;
171 priorityName = CFSTR("verylow");
174 priorityName = CFSTR("moderate");
178 priorityName = CFSTR("normal");
181 priorityName = CFSTR("high");
184 priorityName = CFSTR("emergency");
188 CFMutableStringRef htmlString = CFStringCreateMutableCopy(kCFAllocatorDefault, 0, (CFStringRef)templateHTML);
190 NSString *imageMediaType = @"image/png";
191 NSData *imageData = [icon PNGRepresentation];
193 //Couldn't create a PNG, so fall back on TIFF.
194 imageMediaType = @"image/tiff";
195 imageData = [icon TIFFRepresentation];
197 NSString *growlImageString = [NSString stringWithFormat:@"data:%@;base64,%@", imageMediaType, [imageData base64Encoding]];
199 float opacity = 95.0f;
200 READ_GROWL_PREF_FLOAT(GrowlWebKitOpacityPref, [[bridge display] prefDomain], &opacity);
203 CFStringRef titleHTML = createStringByEscapingForHTML((CFStringRef)title);
204 CFStringRef textHTML = createStringByEscapingForHTML((CFStringRef)text);
205 CFStringRef opacityString = CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%f"), opacity);
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);
214 CFRelease(opacityString);
215 CFRelease(titleHTML);
217 WebFrame *webFrame = [view mainFrame];
218 [[self window] disableFlushWindow];
220 [webFrame loadHTMLString:(NSString *)htmlString baseURL:nil];
221 [[webFrame frameView] setAllowsScrolling:NO];
222 CFRelease(htmlString);
226 * @brief Prevent the webview from following external links. We direct these to the users web browser.
228 - (void) webView:(WebView *)sender
229 decidePolicyForNavigationAction:(NSDictionary *)actionInformation
230 request:(NSURLRequest *)request
231 frame:(WebFrame *)frame
232 decisionListener:(id<WebPolicyDecisionListener>)listener
234 #pragma unused(sender, request, frame)
235 int actionKey = getIntegerForKey(actionInformation, WebActionNavigationTypeKey);
236 if (actionKey == WebNavigationTypeOther) {
239 NSURL *url = getObjectForKey(actionInformation, WebActionOriginalURLKey);
241 //Ignore file URLs, but open anything else
242 if (![url isFileURL])
243 [[NSWorkspace sharedWorkspace] openURL:url];
250 * @brief Invoked once the webview has loaded and is ready to accept content
252 - (void) webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame {
253 #pragma unused(frame)
254 NSWindow *myWindow = [self window];
255 if ([myWindow isFlushWindowDisabled])
256 [myWindow enableFlushWindow];
258 GrowlWebKitWindowView *view = (GrowlWebKitWindowView *)sender;
261 //Update our new frame
262 [[GrowlPositionController sharedInstance] positionDisplay:self];
264 [myWindow invalidateShadow];
267 - (void) setNotification:(GrowlApplicationNotification *)theNotification {
268 if (notification == theNotification)
271 [super setNotification:theNotification];
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);
280 NSPanel *panel = (NSPanel *)[self window];
281 WebView *view = [panel contentView];
282 [self setTitle:title text:text icon:icon priority:priority forView:view];
284 // NSRect panelFrame = [view frame];
286 // [panel setFrame:panelFrame display:NO];
290 #pragma mark positioning methods
292 - (NSPoint) idealOriginInRect:(NSRect)rect {
293 NSRect viewFrame = [[[self window] contentView] frame];
294 enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
297 switch(originatingPosition){
298 case GrowlTopRightPosition:
299 idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
300 NSMaxY(rect) - paddingY - NSHeight(viewFrame));
302 case GrowlTopLeftPosition:
303 idealOrigin = NSMakePoint(NSMinX(rect) + paddingX,
304 NSMaxY(rect) - paddingY - NSHeight(viewFrame));
306 case GrowlBottomLeftPosition:
307 idealOrigin = NSMakePoint(NSMinX(rect) + paddingX,
308 NSMinY(rect) + paddingY);
310 case GrowlBottomRightPosition:
311 idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
312 NSMinY(rect) + paddingY);
315 idealOrigin = NSMakePoint(NSMaxX(rect) - NSWidth(viewFrame) - paddingX,
316 NSMaxY(rect) - paddingY - NSHeight(viewFrame));
323 - (enum GrowlExpansionDirection) primaryExpansionDirection {
324 enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
325 enum GrowlExpansionDirection directionToExpand;
327 switch(originatingPosition){
328 case GrowlTopLeftPosition:
329 directionToExpand = GrowlDownExpansionDirection;
331 case GrowlTopRightPosition:
332 directionToExpand = GrowlDownExpansionDirection;
334 case GrowlBottomLeftPosition:
335 directionToExpand = GrowlUpExpansionDirection;
337 case GrowlBottomRightPosition:
338 directionToExpand = GrowlUpExpansionDirection;
341 directionToExpand = GrowlDownExpansionDirection;
345 return directionToExpand;
348 - (enum GrowlExpansionDirection) secondaryExpansionDirection {
349 enum GrowlPosition originatingPosition = [[GrowlPositionController sharedInstance] originPosition];
350 enum GrowlExpansionDirection directionToExpand;
352 switch(originatingPosition){
353 case GrowlTopLeftPosition:
354 directionToExpand = GrowlRightExpansionDirection;
356 case GrowlTopRightPosition:
357 directionToExpand = GrowlLeftExpansionDirection;
359 case GrowlBottomLeftPosition:
360 directionToExpand = GrowlRightExpansionDirection;
362 case GrowlBottomRightPosition:
363 directionToExpand = GrowlLeftExpansionDirection;
366 directionToExpand = GrowlRightExpansionDirection;
370 return directionToExpand;
373 - (float) requiredDistanceFromExistingDisplays {
379 @implementation NSData (Base64Additions)
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','+','/' };
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;
399 ctremaining = lentext - ixtext;
400 if( ctremaining <= 0 ) break;
402 for( i = 0; i < 3; i++ ) {
404 if( ix < lentext ) inbuf[i] = bytes[ix];
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;
414 switch( ctremaining ) {
423 for( i = 0; i < ctcopy; i++ )
424 [result appendFormat:@"%c", encodingTable[outbuf[i]]];
426 for( i = ctcopy; i < 4; i++ )
427 [result appendString:@"="];
432 if( lineLength > 0 ) {
433 if( charsonline >= lineLength ) {
435 [result appendString:@"\n"];
443 - (NSString *) base64Encoding {
444 return [self base64EncodingWithLineLength:0];
449 @implementation NSImage (PNGRepAddition)
450 - (NSBitmapImageRep *)GrowlBitmapImageRepForPNG
452 //Find the biggest image
453 NSEnumerator *repsEnum = [[self representations] objectEnumerator];
454 NSBitmapImageRep *bestRep = nil;
456 Class NSBitmapImageRepClass = [NSBitmapImageRep class];
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;
476 - (NSData *)PNGRepresentation
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]);