/** 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 Library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "AppKit/NSAttributedString.h" #include "AppKit/NSBezierPath.h" #include "AppKit/NSView.h" #include "AppKit/NSWindow.h" #include "AppKit/NSScreen.h" #include "GNUstepGUI/GSTrackingRect.h" #include "GSToolTips.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 * are instances are stored in the user data field of tracking rectangles. */ @interface GSTTProvider : NSObject { id object; void *data; NSRect viewRect; } - (void*) data; - (id) initWithObject: (id)o userData: (void*)d rect: (NSRect)r; - (id) object; - (void) setObject: (id)o; - (NSRect) viewRect; @end @implementation GSTTProvider - (void*) data { return data; } - (id) initWithObject: (id)o userData: (void*)d rect: (NSRect)r { data = d; object = o; viewRect = r; return self; } - (id) object { return object; } - (void) setObject: (id)o { object = o; } - (NSRect) viewRect { return viewRect; } @end @interface GSToolTips (Private) - (void) _drawText: (NSAttributedString *)text; - (void) _endDisplay; - (void) _timedOut: (NSTimer *)timer; @end typedef struct NSView_struct { @defs(NSView) } *NSViewPtr; @implementation GSToolTips static NSMapTable *viewsMap = 0; static NSTimer *timer; static NSWindow *window; static NSSize offset; static BOOL restoreMouseMoved; + (void) initialize { viewsMap = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks, NSObjectMapValueCallBacks, 8); } + (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 rect: aRect]; 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; timer = nil; window = nil; 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; } provider = (GSTTProvider*)[theEvent userData]; if ([[provider object] respondsToSelector: @selector(view:stringForToolTip:point:userData:)] == YES) { toolTipString = [[provider object] view: view stringForToolTip: [theEvent trackingNumber] point: [theEvent locationInWindow] userData: [provider data]]; } else { toolTipString = [[provider object] description]; } timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 target: self selector: @selector(_timedOut:) userInfo: toolTipString repeats: YES]; if ([[view window] acceptsMouseMovedEvents] == YES) { restoreMouseMoved = NO; } else { restoreMouseMoved = YES; [[view window] setAcceptsMouseMovedEvents: YES]; } [NSWindow _setToolTipVisible: self]; } - (void) mouseExited: (NSEvent *)theEvent { [self _endDisplay]; } - (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) rebuild { NSEnumerator *enumerator; GSTrackingRect *rect; enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; while ((rect = [enumerator nextObject]) != nil) { if (rect->owner == self) { GSTTProvider *provider = (GSTTProvider *)rect->user_data; NSRect frame; if (rect->tag == toolTipTag) { frame = [view bounds]; } else { // FIXME is this the thing to do with tooltips other than // the main one (which we know should cover the whole view)? frame = [provider viewRect]; } frame = [view convertRect: frame toView: nil]; [rect reset: frame inside: NO]; } } } - (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); [view removeTrackingRect: rect->tag]; } } toolTipTag = -1; } - (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); [view removeTrackingRect: tag]; } } } - (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 rect: rect]; 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) _drawText: (NSAttributedString *)text { NSRectEdge sides[] = {NSMinXEdge, NSMaxYEdge, NSMaxXEdge, NSMinYEdge}; NSColor *black = [NSColor blackColor]; NSColor *colors[] = {black, black, black, black}; NSRect bounds = [[window contentView] bounds]; NSRect textRect; textRect = [window frame]; textRect.origin.x = 2; textRect.origin.y = -2; [[window contentView] lockFocus]; [text drawInRect: textRect]; NSDrawColorTiledRects(bounds, bounds, sides, colors, 4); [[window contentView] unlockFocus]; } - (void) _endDisplay { if ([NSWindow _toolTipVisible] == self) { [NSWindow _setToolTipVisible: nil]; } if (timer != nil) { if ([timer isValid]) { [timer invalidate]; } timer = nil; } if (window != nil) { [window close]; window = nil; } if (restoreMouseMoved == YES) { restoreMouseMoved = NO; [[view window] setAcceptsMouseMovedEvents: NO]; } } - (void) _timedOut: (NSTimer *)aTimer { NSString *toolTipString = [aTimer userInfo]; NSAttributedString *toolTipText = nil; NSSize textSize; NSPoint mouseLocation = [NSEvent mouseLocation]; NSRect visible; NSRect rect; NSColor *color; NSMutableDictionary *attributes; if (timer != nil) { if ([timer isValid]) { [timer invalidate]; } timer = nil; } if (window != nil) { /* 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]; } attributes = [NSMutableDictionary dictionary]; [attributes setObject: [NSFont toolTipsFontOfSize: 10.0] forKey: NSFontAttributeName]; toolTipText = [[NSAttributedString alloc] initWithString: toolTipString attributes: attributes]; textSize = [toolTipText size]; /* 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; window = [[NSWindow alloc] initWithContentRect: rect styleMask: NSBorderlessWindowMask backing: NSBackingStoreRetained defer: YES]; color = [NSColor colorWithDeviceRed: 1.0 green: 1.0 blue: 0.90 alpha: 1.0]; [window setBackgroundColor: color]; [window setReleasedWhenClosed: YES]; [window setExcludedFromWindowsMenu: YES]; [window setLevel: NSStatusWindowLevel]; [window orderFront: nil]; [self _drawText: toolTipText]; RELEASE(toolTipText); } @end