/** Implementation of the GSToolTips class Copyright (C) 2006 Free Software Foundation, Inc. Author: Richard Frith-Macdonald Date: 2006 This file is part of the GNUstep GUI Library. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; see the file COPYING.LIB. If not, see or write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #import #import #import #import #import #import #import "AppKit/NSApplication.h" #import "AppKit/NSAttributedString.h" #import "AppKit/NSBezierPath.h" #import "AppKit/NSEvent.h" #import "AppKit/NSScreen.h" #import "AppKit/NSStringDrawing.h" #import "AppKit/NSView.h" #import "AppKit/NSPanel.h" #import "GNUstepGUI/GSTrackingRect.h" #import "GSToolTips.h" #import "GSFastEnumeration.h" @interface NSWindow (GNUstepPrivate) + (void) _setToolTipVisible: (GSToolTips*)t; + (GSToolTips*) _toolTipVisible; @end @interface NSObject (ToolTips) - (NSString*) view: (NSView*)v stringForToolTip: (NSToolTipTag)t point: (NSPoint)p userData: (void*)d; @end /* A trivial class to hold information about the provider of the tooltip * string. Instance allocation/deallocation is managed by GSToolTip and * our instances are stored in the user data field of tracking rectangles. */ @interface GSTTProvider : NSObject { id object; void *data; } - (void*) data; - (id) initWithObject: (id)o userData: (void*)d; - (id) object; - (void) setObject: (id)o; @end @implementation GSTTProvider - (void*) data { return data; } - (void) dealloc { [self setObject: nil]; [super dealloc]; } - (id) initWithObject: (id)o userData: (void*)d { data = d; [self setObject: o]; return self; } - (id) object { return object; } - (void) setObject: (id)o { /* Experimentation on MacOS-X shows that the object is not retained. * However, if the object does not provide a string, we must use a * copy of its description ... and we have to retain that until we * are done with it. */ if ([object respondsToSelector: @selector(view:stringForToolTip:point:userData:)] == NO) { /* Object must be a string rather than something which provides one */ RELEASE(object); } object = o; if ([object respondsToSelector: @selector(view:stringForToolTip:point:userData:)] == NO) { /* Object does not provide a string ... so we take a copy of it * as the string to be used. */ object = [[object description] copy]; } } @end @interface GSTTView : NSView { NSAttributedString *_text; } - (void)setText: (NSAttributedString *)text; @end @implementation GSTTView - (id) initWithFrame: (NSRect)frameRect { self = [super initWithFrame: frameRect]; if (self) { [self setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; } return self; } - (void) setText: (NSAttributedString *)text { if (_text != text) { ASSIGN(_text, text); [self setNeedsDisplay: YES]; } } - (void) drawRect: (NSRect)dirtyRect { if (_text) { NSRectEdge sides[] = {NSMinXEdge, NSMaxYEdge, NSMaxXEdge, NSMinYEdge}; NSColor *black = [NSColor blackColor]; NSColor *colors[] = {black, black, black, black}; NSRect bounds = [self bounds]; NSRect frame = [self frame]; NSRect textRect = NSInsetRect(frame, 2, 2); NSDrawColorTiledRects(bounds, bounds, sides, colors, 4); [_text drawInRect: textRect]; } } @end @interface GSTTPanel : NSPanel // Tooltip panel that will not try to become main or key - (BOOL) canBecomeKeyWindow; - (BOOL) canBecomeMainWindow; @end @implementation GSTTPanel - (id) initWithContentRect: (NSRect)contentRect styleMask: (NSUInteger)aStyle backing: (NSBackingStoreType)bufferingType defer: (BOOL)flag { self = [super initWithContentRect: contentRect styleMask: aStyle backing: bufferingType defer: flag]; if (self) { [self setContentView: AUTORELEASE([[GSTTView alloc] initWithFrame: contentRect])]; } return self; } - (BOOL) canBecomeKeyWindow { return NO; } - (BOOL) canBecomeMainWindow { return NO; } @end @interface GSToolTips (Private) - (void) _endDisplay; - (void) _endDisplay: (NSTrackingRectTag)tag; - (void) _timedOut: (NSTimer *)timer; @end /* typedef struct NSView_struct { @defs(NSView) } *NSViewPtr; */ typedef NSView* NSViewPtr; @implementation GSToolTips static NSMapTable *viewsMap = 0; static NSTimer *timer = nil; static GSToolTips *timedObject = nil; static NSTrackingRectTag timedTag = NSNotFound; // Having a single stored panel for tooltips greatly reduces callback interaction from MS-Windows static GSTTPanel *window = nil; // Prevent Windows callback API from attempting to dismiss tooltip as its in the process of appearing static BOOL isOpening = NO; static NSSize offset; static BOOL restoreMouseMoved; + (void) initialize { viewsMap = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks, NSObjectMapValueCallBacks, 8); window = [[GSTTPanel alloc] initWithContentRect: NSMakeRect(0,0,100,25) styleMask: NSBorderlessWindowMask backing: NSBackingStoreRetained defer: YES]; [window setBackgroundColor: [NSColor toolTipColor]]; [window setReleasedWhenClosed: NO]; [window setExcludedFromWindowsMenu: YES]; [window setLevel: NSPopUpMenuWindowLevel]; [window setAutodisplay: NO]; } + (void) removeTipsForView: (NSView*)aView { GSToolTips *tt = (GSToolTips*)NSMapGet(viewsMap, (void*)aView); if (tt != nil) { [tt removeAllToolTips]; NSMapRemove(viewsMap, (void*)aView); } } + (GSToolTips*) tipsForView: (NSView*)aView { GSToolTips *tt = (GSToolTips*)NSMapGet(viewsMap, (void*)aView); if (tt == nil) { tt = [[GSToolTips alloc] initForView: aView]; NSMapInsert(viewsMap, (void*)aView, (void*)tt); RELEASE(tt); } return tt; } - (NSToolTipTag) addToolTipRect: (NSRect)aRect owner: (id)anObject userData: (void *)data { NSTrackingRectTag tag; GSTTProvider *provider; if (timer != nil) { return -1; // A tip is already in progress. } aRect = NSIntersectionRect(aRect, [view bounds]); if (NSEqualRects(aRect, NSZeroRect)) { return -1; // No rectangle. } if (anObject == nil) { return -1; // No provider object. } provider = [[GSTTProvider alloc] initWithObject: anObject userData: data]; tag = [view addTrackingRect: aRect owner: self userData: provider assumeInside: NO]; return tag; } - (unsigned) count { NSEnumerator *enumerator; GSTrackingRect *rect; unsigned count = 0; enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; while ((rect = [enumerator nextObject]) != nil) { if (rect->owner == self) { count++; } } return count; } - (void) dealloc { [self _endDisplay]; [self removeAllToolTips]; [super dealloc]; } - (id) initForView: (NSView*)aView { view = aView; toolTipTag = -1; return self; } - (void) mouseEntered: (NSEvent *)theEvent { GSTTProvider *provider; NSString *toolTipString; if (timer != nil) { /* Moved from one tooltip view to another, so reset the timer. */ [timer invalidate]; timer = nil; timedObject = nil; timedTag = NSNotFound; } provider = (GSTTProvider*)[theEvent userData]; if ([[provider object] respondsToSelector: @selector(view:stringForToolTip:point:userData:)] == YES) { // From testing on OS X, point is in the view's coordinate system // The locationInWindow has been converted to this in // [NSWindow _checkTrackingRectangles:forEvent:] NSPoint p = [theEvent locationInWindow]; toolTipString = [[provider object] view: view stringForToolTip: [theEvent trackingNumber] point: p userData: [provider data]]; } else { toolTipString = [provider object]; } timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 target: self selector: @selector(_timedOut:) userInfo: toolTipString repeats: YES]; [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSModalPanelRunLoopMode]; timedObject = self; timedTag = [theEvent trackingNumber]; if ([[view window] acceptsMouseMovedEvents] == YES) { restoreMouseMoved = NO; } else { restoreMouseMoved = YES; [[view window] setAcceptsMouseMovedEvents: YES]; } [NSWindow _setToolTipVisible: self]; } - (void) mouseExited: (NSEvent *)theEvent { [self _endDisplay:[theEvent trackingNumber]]; } - (void) mouseDown: (NSEvent *)theEvent { [self _endDisplay]; } - (void) mouseMoved: (NSEvent *)theEvent { NSPoint mouseLocation; NSPoint origin; if (window == nil) { return; } mouseLocation = [NSEvent mouseLocation]; origin = NSMakePoint(mouseLocation.x + offset.width, mouseLocation.y + offset.height); [window setFrameOrigin: origin]; } - (void) removeAllToolTips { NSEnumerator *enumerator; GSTrackingRect *rect; [self _endDisplay]; enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; while ((rect = [enumerator nextObject]) != nil) { if (rect->owner == self) { RELEASE((GSTTProvider*)rect->user_data); rect->user_data = 0; [view removeTrackingRect: rect->tag]; } } toolTipTag = -1; } - (void)removeToolTipsInRect: (NSRect)aRect { NSUInteger idx = 0; NSMutableIndexSet *indexes = [NSMutableIndexSet new]; id tracking_rects = ((NSViewPtr)view)->_tracking_rects; FOR_IN(GSTrackingRect*, rect, tracking_rects) if ((rect->owner == self) && NSContainsRect(aRect, rect->rectangle)) { RELEASE((GSTTProvider*)rect->user_data); rect->user_data = 0; [indexes addIndex: idx]; [rect invalidate]; } idx++; END_FOR_IN(tracking_rects) [((NSViewPtr)view)->_tracking_rects removeObjectsAtIndexes: indexes]; if ([((NSViewPtr)view)->_tracking_rects count] == 0) { ((NSViewPtr)view)->_rFlags.has_trkrects = 0; } RELEASE(indexes); } - (void) removeToolTip: (NSToolTipTag)tag { NSEnumerator *enumerator; GSTrackingRect *rect; enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; while ((rect = [enumerator nextObject]) != nil) { if (rect->tag == tag && rect->owner == self) { RELEASE((GSTTProvider*)rect->user_data); rect->user_data = 0; [view removeTrackingRect: tag]; return; } } } - (void) setToolTip: (NSString *)string { if ([string length] == 0) { if (toolTipTag != -1) { [self _endDisplay]; [self removeToolTip: toolTipTag]; toolTipTag = -1; } } else { GSTTProvider *provider; if (toolTipTag == -1) { NSRect rect; rect = [view bounds]; provider = [[GSTTProvider alloc] initWithObject: string userData: nil]; toolTipTag = [view addTrackingRect: rect owner: self userData: provider assumeInside: NO]; } else { NSEnumerator *enumerator; GSTrackingRect *rect; enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; while ((rect = [enumerator nextObject]) != nil) { if (rect->tag == toolTipTag && rect->owner == self) { [((GSTTProvider*)rect->user_data) setObject: string]; } } } } } - (NSString *) toolTip { NSEnumerator *enumerator; GSTrackingRect *rect; enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; while ((rect = [enumerator nextObject]) != nil) { if (rect->tag == toolTipTag) { return [((GSTTProvider*)rect->user_data) object]; } } return nil; } @end @implementation GSToolTips (Private) - (void) _endDisplay { [self _endDisplay:NSNotFound]; } - (void) _endDisplay: (NSTrackingRectTag)tag { if (isOpening) return; if ([NSWindow _toolTipVisible] == self) { [NSWindow _setToolTipVisible: nil]; } /* If there is currently a timer running for this object and it is the target tag, * cancel it. Always remove if the target tag is NSNotFound */ if (timer != nil && timedObject == self && (timedTag == tag || tag == NSNotFound)) { if ([timer isValid]) { [timer invalidate]; } timer = nil; timedObject = nil; timedTag = NSNotFound; } if (window != nil) { [window setFrame: NSZeroRect display: NO]; [window orderOut:self]; } if (restoreMouseMoved == YES) { restoreMouseMoved = NO; [[view window] setAcceptsMouseMovedEvents: NO]; } } /* The delay timed out -- display the tooltip */ - (void) _timedOut: (NSTimer *)aTimer { CGFloat size; NSString *toolTipString; NSAttributedString *toolTipText = nil; NSSize textSize; NSPoint mouseLocation = [NSEvent mouseLocation]; NSRect visible; NSRect rect; NSMutableDictionary *attributes; // retain and autorelease the timer's userinfo because we // may invalidate the timer (which releases the userinfo), // but need the userinfo object to remain valid for the // remainder of this method. toolTipString = [[[aTimer userInfo] retain] autorelease]; if ( (nil == toolTipString) || ([toolTipString isEqualToString: @""]) ) { return; } if (timer != nil) { if ([timer isValid]) { [timer invalidate]; } timer = nil; timedObject = nil; timedTag = NSNotFound; } if ([window isVisible]) { /* Moved from one tooltip view to another ... so stop displaying * the old tool tip before we start the new one. * This is similar to the case in -mouseEntered: where we cancel * the timer for one tooltip view because we have entered another * one. * To think about ... if we entered a tooltip rectangle without * having left the previous one, then when we leave this rectangle * we are probably back in the other one and should really restart * the timer for the original view. However, this is a rare case * so it's probably better to ignore it than add a lot of code to * keep track of all entry and exit. */ [self _endDisplay]; } size = [[NSUserDefaults standardUserDefaults] floatForKey: @"NSToolTipsFontSize"]; if (size <= 0) { size = 10.0; } attributes = [NSMutableDictionary dictionary]; [attributes setObject: [NSFont toolTipsFontOfSize: size] forKey: NSFontAttributeName]; [attributes setObject: [NSColor toolTipTextColor] forKey: NSForegroundColorAttributeName]; toolTipText = [[NSAttributedString alloc] initWithString: toolTipString attributes: attributes]; textSize = [toolTipText size]; if (textSize.width > 300) { NSRect rect; rect = [toolTipText boundingRectWithSize: NSMakeSize(300, 1e7) options: 0]; textSize = rect.size; } // Need to fudge the result just a bit due to occasionally cutoff characters... textSize.width += 1; textSize.height += 1; /* Create window just off the current mouse position * Constrain it to be on screen, shrinking if necessary. */ rect = NSMakeRect(mouseLocation.x + 8, mouseLocation.y - 16 - (textSize.height+3), textSize.width + 4, textSize.height + 4); visible = [[NSScreen mainScreen] visibleFrame]; if (NSMaxY(rect) > NSMaxY(visible)) { rect.origin.y -= (NSMaxY(rect) - NSMaxY(visible)); } if (NSMinY(rect) < NSMinY(visible)) { rect.origin.y += (NSMinY(visible) - NSMinY(rect)); } if (NSMaxY(rect) > NSMaxY(visible)) { rect.origin.y = visible.origin.y; rect.size.height = visible.size.height; } if (NSMaxX(rect) > NSMaxX(visible)) { rect.origin.x -= (NSMaxX(rect) - NSMaxX(visible)); } if (NSMinX(rect) < NSMinX(visible)) { rect.origin.x += (NSMinX(visible) - NSMinX(rect)); } if (NSMaxX(rect) > NSMaxX(visible)) { rect.origin.x = visible.origin.x; rect.size.width = visible.size.width; } offset.height = rect.origin.y - mouseLocation.y; offset.width = rect.origin.x - mouseLocation.x; isOpening = YES; [(GSTTView*)([window contentView]) setText: toolTipText]; [window setFrame: rect display: NO]; [window orderFront: nil]; isOpening = NO; RELEASE(toolTipText); } @end