//
// $Id$
//
// SPTooltip.m
// sequel-pro
//
// Created by Hans-J. Bibiko on August 11, 2009.
//
// This class is based on TextMate's TMDHTMLTip implementation
// (Dialog plugin) written by CiarĂ¡n Walsh and Allan Odgaard.
// see license: http://svn.textmate.org/trunk/LICENSE
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// More info at
", ([displayOptions objectForKey:@"fontname"]) ? [displayOptions objectForKey:@"fontname"] : @"Lucida Grande"] atIndex:0]; [text appendString:@""]; html = text; } else { html = @"Error"; } [tip setContent:html withOptions:displayOptions]; } else if([type isEqualToString:@"html"]) { [tip setContent:(NSString*)content withOptions:displayOptions]; } else if([type isEqualToString:@"image"]) { [tip setBackgroundColor:[NSColor clearColor]]; [tip setOpaque:NO]; [tip setLevel:NSNormalWindowLevel]; [tip setExcludedFromWindowsMenu:YES]; [tip setAlphaValue:1]; NSSize s = [(NSImage *)content size]; // Downsize a large image NSInteger w = s.width; NSInteger h = s.height; if(w>h) { if(s.width > 200) { w = 200; h = 200/s.width*s.height; } } else { if(s.height > 200) { h = 200; w = 200/s.height*s.width; } } // Show image in a NSImageView NSImageView *backgroundImageView = [[NSImageView alloc] initWithFrame:NSMakeRect(0,0,w, h)]; [backgroundImageView setImage:(NSImage *)content]; [backgroundImageView setFrameSize:NSMakeSize(w, h)]; [tip setContentView:backgroundImageView]; [tip setContentSize:NSMakeSize(w,h)]; [tip setFrameTopLeftPoint:point]; [tip sizeToContent]; [tip orderFront:self]; [tip performSelector:@selector(runUntilUserActivity) withObject:nil afterDelay:0]; [backgroundImageView release]; } else { [tip setContent:(NSString*)content withOptions:displayOptions]; NSBeep(); NSLog(@"SPTooltip: Type '%@' is not supported. Please use 'text' or 'html'. Tooltip is displayed as type 'html'", type); } } - (void)initMeWithOptions:(NSDictionary *)displayOptions { [self setReleasedWhenClosed:YES]; [self setAlphaValue:0.97f]; [self setOpaque:NO]; [self setBackgroundColor:[NSColor colorWithDeviceRed:1.0f green:0.96f blue:0.76f alpha:1.0f]]; [self setBackgroundColor:[NSColor clearColor]]; [self setHasShadow:YES]; [self setLevel:NSStatusWindowLevel]; [self setHidesOnDeactivate:YES]; [self setIgnoresMouseEvents:YES]; webPreferences = [[WebPreferences alloc] initWithIdentifier:@"SequelPro Tooltip"]; [webPreferences setJavaScriptEnabled:YES]; NSString *fontName = ([displayOptions objectForKey:@"fontname"]) ? [displayOptions objectForKey:@"fontname"] : @"Lucida Grande"; NSInteger fontSize = ([displayOptions objectForKey:@"fontsize"]) ? [[displayOptions objectForKey:@"fontsize"] integerValue] : 10; if(fontSize < 5) fontSize = 5; NSFont* font = [NSFont fontWithName:fontName size:fontSize]; [webPreferences setStandardFontFamily:[font familyName]]; [webPreferences setDefaultFontSize:fontSize]; [webPreferences setDefaultFixedFontSize:fontSize]; webView = [[WebView alloc] initWithFrame:NSZeroRect]; [webView setPreferencesIdentifier:@"SequelPro Tooltip"]; [webView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; [webView setFrameLoadDelegate:self]; if ([webView respondsToSelector:@selector(setDrawsBackground:)]) [webView setDrawsBackground:NO]; [self setContentView:webView]; } - (id)init; { if(self = [self initWithContentRect:NSMakeRect(1,1,1,1) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]) { ; } return self; } - (void)dealloc { [NSObject cancelPreviousPerformRequestsWithTarget:self]; [didOpenAtDate release]; [webView release]; [webPreferences release]; [super dealloc]; } + (void)setDisplayOptions:(NSDictionary *)aDict { // displayOptions = [NSDictionary dictionaryWithDictionary:aDict]; } + (NSPoint)caretPosition { NSPoint pos; id fr = [[NSApp keyWindow] firstResponder]; //If first responder is a textview return the caret position if([fr respondsToSelector:@selector(getRangeForCurrentWord)] ) { NSRange range = NSMakeRange([fr selectedRange].location,0); NSRange glyphRange = [[fr layoutManager] glyphRangeForCharacterRange:range actualCharacterRange:NULL]; NSRect boundingRect = [[fr layoutManager] boundingRectForGlyphRange:glyphRange inTextContainer:[fr textContainer]]; boundingRect = [fr convertRect: boundingRect toView: NULL]; pos = [[fr window] convertBaseToScreen: NSMakePoint(boundingRect.origin.x + boundingRect.size.width,boundingRect.origin.y + boundingRect.size.height)]; NSFont* font = [fr font]; pos.y -= [font pointSize]*1.3; return pos; // Otherwise return the upper left corner of the current keyWindow } else { pos = [NSEvent mouseLocation]; pos.y -= 16; return pos; // pos = [[NSApp keyWindow] frame].origin; // pos.x += 5; // pos.y += [[NSApp keyWindow] frame].size.height - 23; // return pos; } } // =========== // = Webview = // =========== - (void)setContent:(NSString *)content withOptions:(NSDictionary *)displayOptions { NSString *fullContent = @"" @"" @" " @"" @"%@" @""; NSString *bgColor = ([displayOptions objectForKey:@"backgroundcolor"]) ? [displayOptions objectForKey:@"backgroundcolor"] : @"#F9FBC5"; BOOL transparent = ([displayOptions objectForKey:@"transparent"]) ? YES : NO; fullContent = [NSString stringWithFormat:fullContent, transparent ? @"transparent" : bgColor, content]; [[webView mainFrame] loadHTMLString:fullContent baseURL:nil]; } - (void)sizeToContent { NSRect frame; // Current tooltip position NSPoint pos = NSMakePoint([self frame].origin.x, [self frame].origin.y + [self frame].size.height); // Find the screen which we are displaying on NSRect screenFrame = [[NSScreen mainScreen] frame]; NSScreen* candidate; for(candidate in [NSScreen screens]) { if(NSMinX([candidate frame]) < pos.x && NSMinX([candidate frame]) > NSMinX(screenFrame)) screenFrame = [candidate frame]; } // is contentView a webView calculate actual rendered size via JavaScript if([[[[self contentView] class] description] isEqualToString:@"WebView"]) { // The webview is set to a large initial size and then sized down to fit the content [self setContentSize:NSMakeSize(screenFrame.size.width - screenFrame.size.width / 3.0f , screenFrame.size.height)]; NSInteger height = [[[webView windowScriptObject] evaluateWebScript:@"document.body.offsetHeight + document.body.offsetTop;"] integerValue]; NSInteger width = [[[webView windowScriptObject] evaluateWebScript:@"document.body.offsetWidth + document.body.offsetLeft;"] integerValue]; [webView setFrameSize:NSMakeSize(width, height)]; frame = [self frameRectForContentRect:[webView frame]]; } else { frame = [self frame]; } //Adjust frame to fit into the screenFrame frame.size.width = MIN(NSWidth(frame), NSWidth(screenFrame)); frame.size.height = MIN(NSHeight(frame), NSHeight(screenFrame)); [self setFrame:frame display:NO]; //Adjust tooltip origin to fit into the screenFrame pos.x = MAX(NSMinX(screenFrame), MIN(pos.x, NSMaxX(screenFrame)-NSWidth(frame))); pos.y = MIN(MAX(NSMinY(screenFrame)+NSHeight(frame), pos.y), NSMaxY(screenFrame)); [self setFrameTopLeftPoint:pos]; } - (void)webView:(WebView*)sender didFinishLoadForFrame:(WebFrame*)frame; { [self sizeToContent]; [self orderFront:self]; [self performSelector:@selector(runUntilUserActivity) withObject:nil afterDelay:0]; } // ================== // = Event handling = // ================== - (BOOL)shouldCloseForMousePosition:(NSPoint)aPoint { CGFloat ignorePeriod = 0.05f; if(-[didOpenAtDate timeIntervalSinceNow] < ignorePeriod) return NO; if(NSEqualPoints(mousePositionWhenOpened, NSZeroPoint)) { mousePositionWhenOpened = aPoint; return NO; } NSPoint p = mousePositionWhenOpened; CGFloat deltaX = p.x - aPoint.x; CGFloat deltaY = p.y - aPoint.y; CGFloat dist = sqrt(deltaX * deltaX + deltaY * deltaY); CGFloat moveThreshold = 10; return dist > moveThreshold; } - (void)runUntilUserActivity { [self setValue:[NSDate date] forKey:@"didOpenAtDate"]; mousePositionWhenOpened = NSZeroPoint; NSWindow* keyWindow = [[NSApp keyWindow] retain]; BOOL didAcceptMouseMovedEvents = [keyWindow acceptsMouseMovedEvents]; [keyWindow setAcceptsMouseMovedEvents:YES]; NSEvent* event = nil; NSInteger eventType; while(event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantFuture] inMode:NSDefaultRunLoopMode dequeue:YES]) { eventType = [event type]; if(eventType == NSKeyDown || eventType == NSLeftMouseDown || eventType == NSRightMouseDown || eventType == NSOtherMouseDown || eventType == NSScrollWheel) break; if(eventType == NSMouseMoved && [self shouldCloseForMousePosition:[NSEvent mouseLocation]]) break; if(keyWindow != [NSApp keyWindow] || ![NSApp isActive]) break; if(spTooltipCounter > 1) break; [NSApp sendEvent:event]; } [keyWindow setAcceptsMouseMovedEvents:didAcceptMouseMovedEvents]; [keyWindow release]; [self orderOut:self]; // If we still have an event, pass it on to the app to ensure all actions are performed if (event) [NSApp sendEvent:event]; } // ============= // = Animation = // ============= - (void)orderOut:(id)sender { if(![self isVisible] || animationTimer) return; [self stopAnimation:self]; [self setValue:[NSDate date] forKey:@"animationStart"]; [self setValue:[NSTimer scheduledTimerWithTimeInterval:0.01f target:self selector:@selector(animationTick:) userInfo:nil repeats:YES] forKey:@"animationTimer"]; } - (void)animationTick:(id)sender { CGFloat alpha = 0.97f * (1.0f - 40*slow_in_out(-2.2 * [animationStart timeIntervalSinceNow])); if(alpha > 0.0f && spTooltipCounter==1) { [self setAlphaValue:alpha]; } else { [super orderOut:self]; [self stopAnimation:self]; [self close]; spTooltipCounter--; if(spTooltipCounter < 0) spTooltipCounter = 0; } } - (void)stopAnimation:(id)sender; { if(animationTimer) { [[self retain] autorelease]; [animationTimer invalidate]; [self setValue:nil forKey:@"animationTimer"]; [self setValue:nil forKey:@"animationStart"]; [self setAlphaValue:0.97f]; } } @end