1/** 2 Implementation of the GSToolTips class 3 4 Copyright (C) 2006 Free Software Foundation, Inc. 5 6 Author: Richard Frith-Macdonald <richard@brainstorm.co.uk> 7 Date: 2006 8 9 This file is part of the GNUstep GUI Library. 10 11 This library is free software; you can redistribute it and/or 12 modify it under the terms of the GNU Lesser General Public 13 License as published by the Free Software Foundation; either 14 version 2 of the License, or (at your option) any later version. 15 16 This library is distributed in the hope that it will be useful, 17 but WITHOUT ANY WARRANTY; without even the implied warranty of 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 19 Lesser General Public License for more details. 20 21 You should have received a copy of the GNU Lesser General Public 22 License along with this library; see the file COPYING.LIB. 23 If not, see <http://www.gnu.org/licenses/> or write to the 24 Free Software Foundation, 51 Franklin Street, Fifth Floor, 25 Boston, MA 02110-1301, USA. 26*/ 27 28#import <Foundation/NSGeometry.h> 29#import <Foundation/NSRunLoop.h> 30#import <Foundation/NSString.h> 31#import <Foundation/NSTimer.h> 32#import <Foundation/NSIndexSet.h> 33#import <Foundation/NSUserDefaults.h> 34 35#import "AppKit/NSApplication.h" 36#import "AppKit/NSAttributedString.h" 37#import "AppKit/NSBezierPath.h" 38#import "AppKit/NSEvent.h" 39#import "AppKit/NSScreen.h" 40#import "AppKit/NSStringDrawing.h" 41#import "AppKit/NSView.h" 42#import "AppKit/NSPanel.h" 43#import "GNUstepGUI/GSTrackingRect.h" 44#import "GSToolTips.h" 45#import "GSFastEnumeration.h" 46 47@interface NSWindow (GNUstepPrivate) 48 49+ (void) _setToolTipVisible: (GSToolTips*)t; 50+ (GSToolTips*) _toolTipVisible; 51 52@end 53 54 55@interface NSObject (ToolTips) 56- (NSString*) view: (NSView*)v stringForToolTip: (NSToolTipTag)t 57 point: (NSPoint)p userData: (void*)d; 58@end 59 60/* A trivial class to hold information about the provider of the tooltip 61 * string. Instance allocation/deallocation is managed by GSToolTip and 62 * our instances are stored in the user data field of tracking rectangles. 63 */ 64@interface GSTTProvider : NSObject 65{ 66 id object; 67 void *data; 68} 69- (void*) data; 70- (id) initWithObject: (id)o userData: (void*)d; 71- (id) object; 72- (void) setObject: (id)o; 73@end 74 75@implementation GSTTProvider 76- (void*) data 77{ 78 return data; 79} 80- (void) dealloc 81{ 82 [self setObject: nil]; 83 [super dealloc]; 84} 85- (id) initWithObject: (id)o userData: (void*)d 86{ 87 data = d; 88 [self setObject: o]; 89 return self; 90} 91- (id) object 92{ 93 return object; 94} 95- (void) setObject: (id)o 96{ 97 /* Experimentation on MacOS-X shows that the object is not retained. 98 * However, if the object does not provide a string, we must use a 99 * copy of its description ... and we have to retain that until we 100 * are done with it. 101 */ 102 if ([object respondsToSelector: 103 @selector(view:stringForToolTip:point:userData:)] == NO) 104 { 105 /* Object must be a string rather than something which provides one */ 106 RELEASE(object); 107 } 108 object = o; 109 if ([object respondsToSelector: 110 @selector(view:stringForToolTip:point:userData:)] == NO) 111 { 112 /* Object does not provide a string ... so we take a copy of it 113 * as the string to be used. 114 */ 115 object = [[object description] copy]; 116 } 117} 118@end 119 120@interface GSTTView : NSView 121{ 122 NSAttributedString *_text; 123} 124 125- (void)setText: (NSAttributedString *)text; 126@end 127 128@implementation GSTTView 129- (id) initWithFrame: (NSRect)frameRect 130{ 131 self = [super initWithFrame: frameRect]; 132 if (self) 133 { 134 [self setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable]; 135 } 136 return self; 137} 138 139- (void) setText: (NSAttributedString *)text 140{ 141 if (_text != text) 142 { 143 ASSIGN(_text, text); 144 [self setNeedsDisplay: YES]; 145 } 146} 147 148- (void) drawRect: (NSRect)dirtyRect 149{ 150 if (_text) 151 { 152 NSRectEdge sides[] = {NSMinXEdge, NSMaxYEdge, NSMaxXEdge, NSMinYEdge}; 153 NSColor *black = [NSColor blackColor]; 154 NSColor *colors[] = {black, black, black, black}; 155 NSRect bounds = [self bounds]; 156 NSRect frame = [self frame]; 157 NSRect textRect = NSInsetRect(frame, 2, 2); 158 159 NSDrawColorTiledRects(bounds, bounds, sides, colors, 4); 160 [_text drawInRect: textRect]; 161 } 162} 163@end 164 165@interface GSTTPanel : NSPanel 166// Tooltip panel that will not try to become main or key 167- (BOOL) canBecomeKeyWindow; 168- (BOOL) canBecomeMainWindow; 169 170@end 171 172@implementation GSTTPanel 173 174- (id) initWithContentRect: (NSRect)contentRect 175 styleMask: (NSUInteger)aStyle 176 backing: (NSBackingStoreType)bufferingType 177 defer: (BOOL)flag; 178{ 179 self = [super initWithContentRect: contentRect 180 styleMask: aStyle 181 backing: bufferingType 182 defer: flag]; 183 if (self) 184 { 185 [self setContentView: [[[GSTTView alloc] initWithFrame: contentRect] autorelease]]; 186 } 187 return self; 188} 189 190- (BOOL) canBecomeKeyWindow 191{ 192 return NO; 193} 194 195- (BOOL) canBecomeMainWindow 196{ 197 return NO; 198} 199 200@end 201 202 203@interface GSToolTips (Private) 204- (void) _endDisplay; 205- (void) _endDisplay: (NSTrackingRectTag)tag; 206- (void) _timedOut: (NSTimer *)timer; 207@end 208/* 209typedef struct NSView_struct 210{ 211 @defs(NSView) 212} *NSViewPtr; 213*/ 214typedef NSView* NSViewPtr; 215 216@implementation GSToolTips 217 218static NSMapTable *viewsMap = 0; 219static NSTimer *timer = nil; 220static GSToolTips *timedObject = nil; 221static NSTrackingRectTag timedTag = NSNotFound; 222// Having a single stored panel for tooltips greatly reduces callback interaction from MS-Windows 223static GSTTPanel *window = nil; 224// Prevent Windows callback API from attempting to dismiss tooltip as its in the process of appearing 225static BOOL isOpening = NO; 226static NSSize offset; 227static BOOL restoreMouseMoved; 228 229+ (void) initialize 230{ 231 viewsMap = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks, 232 NSObjectMapValueCallBacks, 8); 233 234 window = [[GSTTPanel alloc] initWithContentRect: NSMakeRect(0,0,100,25) 235 styleMask: NSBorderlessWindowMask 236 backing: NSBackingStoreRetained 237 defer: YES]; 238 239 [window setBackgroundColor: [NSColor toolTipColor]]; 240 [window setReleasedWhenClosed: NO]; 241 [window setExcludedFromWindowsMenu: YES]; 242 [window setLevel: NSPopUpMenuWindowLevel]; 243 [window setAutodisplay: NO]; 244} 245 246+ (void) removeTipsForView: (NSView*)aView 247{ 248 GSToolTips *tt = (GSToolTips*)NSMapGet(viewsMap, (void*)aView); 249 250 if (tt != nil) 251 { 252 [tt removeAllToolTips]; 253 NSMapRemove(viewsMap, (void*)aView); 254 } 255} 256 257+ (GSToolTips*) tipsForView: (NSView*)aView 258{ 259 GSToolTips *tt = (GSToolTips*)NSMapGet(viewsMap, (void*)aView); 260 261 if (tt == nil) 262 { 263 tt = [[GSToolTips alloc] initForView: aView]; 264 NSMapInsert(viewsMap, (void*)aView, (void*)tt); 265 RELEASE(tt); 266 } 267 return tt; 268} 269 270 271 272- (NSToolTipTag) addToolTipRect: (NSRect)aRect 273 owner: (id)anObject 274 userData: (void *)data 275{ 276 NSTrackingRectTag tag; 277 GSTTProvider *provider; 278 279 if (timer != nil) 280 { 281 return -1; // A tip is already in progress. 282 } 283 aRect = NSIntersectionRect(aRect, [view bounds]); 284 if (NSEqualRects(aRect, NSZeroRect)) 285 { 286 return -1; // No rectangle. 287 } 288 if (anObject == nil) 289 { 290 return -1; // No provider object. 291 } 292 293 provider = [[GSTTProvider alloc] initWithObject: anObject 294 userData: data]; 295 tag = [view addTrackingRect: aRect 296 owner: self 297 userData: provider 298 assumeInside: NO]; 299 return tag; 300} 301 302- (unsigned) count 303{ 304 NSEnumerator *enumerator; 305 GSTrackingRect *rect; 306 unsigned count = 0; 307 308 enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; 309 while ((rect = [enumerator nextObject]) != nil) 310 { 311 if (rect->owner == self) 312 { 313 count++; 314 } 315 } 316 return count; 317} 318 319- (void) dealloc 320{ 321 [self _endDisplay]; 322 [self removeAllToolTips]; 323 [super dealloc]; 324} 325 326- (id) initForView: (NSView*)aView 327{ 328 view = aView; 329 toolTipTag = -1; 330 return self; 331} 332 333- (void) mouseEntered: (NSEvent *)theEvent 334{ 335 GSTTProvider *provider; 336 NSString *toolTipString; 337 338 if (timer != nil) 339 { 340 /* Moved from one tooltip view to another, so reset the timer. 341 */ 342 [timer invalidate]; 343 timer = nil; 344 timedObject = nil; 345 timedTag = NSNotFound; 346 } 347 348 provider = (GSTTProvider*)[theEvent userData]; 349 if ([[provider object] respondsToSelector: 350 @selector(view:stringForToolTip:point:userData:)] == YES) 351 { 352 // From testing on OS X, point is in the view's coordinate system 353 // The locationInWindow has been converted to this in 354 // [NSWindow _checkTrackingRectangles:forEvent:] 355 NSPoint p = [theEvent locationInWindow]; 356 357 toolTipString = [[provider object] view: view 358 stringForToolTip: [theEvent trackingNumber] 359 point: p 360 userData: [provider data]]; 361 } 362 else 363 { 364 toolTipString = [provider object]; 365 } 366 367 timer = [NSTimer scheduledTimerWithTimeInterval: 0.5 368 target: self 369 selector: @selector(_timedOut:) 370 userInfo: toolTipString 371 repeats: YES]; 372 [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSModalPanelRunLoopMode]; 373 timedObject = self; 374 timedTag = [theEvent trackingNumber]; 375 if ([[view window] acceptsMouseMovedEvents] == YES) 376 { 377 restoreMouseMoved = NO; 378 } 379 else 380 { 381 restoreMouseMoved = YES; 382 [[view window] setAcceptsMouseMovedEvents: YES]; 383 } 384 [NSWindow _setToolTipVisible: self]; 385} 386 387- (void) mouseExited: (NSEvent *)theEvent 388{ 389 [self _endDisplay:[theEvent trackingNumber]]; 390} 391 392- (void) mouseDown: (NSEvent *)theEvent 393{ 394 [self _endDisplay]; 395} 396 397- (void) mouseMoved: (NSEvent *)theEvent 398{ 399 NSPoint mouseLocation; 400 NSPoint origin; 401 402 if (window == nil) 403 { 404 return; 405 } 406 407 mouseLocation = [NSEvent mouseLocation]; 408 409 origin = NSMakePoint(mouseLocation.x + offset.width, 410 mouseLocation.y + offset.height); 411 412 [window setFrameOrigin: origin]; 413} 414 415- (void) removeAllToolTips 416{ 417 NSEnumerator *enumerator; 418 GSTrackingRect *rect; 419 420 [self _endDisplay]; 421 422 enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; 423 while ((rect = [enumerator nextObject]) != nil) 424 { 425 if (rect->owner == self) 426 { 427 RELEASE((GSTTProvider*)rect->user_data); 428 rect->user_data = 0; 429 [view removeTrackingRect: rect->tag]; 430 } 431 } 432 toolTipTag = -1; 433} 434 435- (void)removeToolTipsInRect: (NSRect)aRect 436{ 437 NSUInteger idx = 0; 438 NSMutableIndexSet *indexes = [NSMutableIndexSet new]; 439 id tracking_rects = ((NSViewPtr)view)->_tracking_rects; 440 FOR_IN(GSTrackingRect*, rect, tracking_rects) 441 if ((rect->owner == self) && NSContainsRect(aRect, rect->rectangle)) 442 { 443 RELEASE((GSTTProvider*)rect->user_data); 444 rect->user_data = 0; 445 [indexes addIndex: idx]; 446 [rect invalidate]; 447 } 448 idx++; 449 END_FOR_IN(tracking_rects) 450 [((NSViewPtr)view)->_tracking_rects removeObjectsAtIndexes: indexes]; 451 if ([((NSViewPtr)view)->_tracking_rects count] == 0) 452 { 453 ((NSViewPtr)view)->_rFlags.has_trkrects = 0; 454 } 455 [indexes release]; 456} 457 458- (void) removeToolTip: (NSToolTipTag)tag 459{ 460 NSEnumerator *enumerator; 461 GSTrackingRect *rect; 462 463 enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; 464 while ((rect = [enumerator nextObject]) != nil) 465 { 466 if (rect->tag == tag && rect->owner == self) 467 { 468 RELEASE((GSTTProvider*)rect->user_data); 469 rect->user_data = 0; 470 [view removeTrackingRect: tag]; 471 return; 472 } 473 } 474} 475 476- (void) setToolTip: (NSString *)string 477{ 478 if ([string length] == 0) 479 { 480 if (toolTipTag != -1) 481 { 482 [self _endDisplay]; 483 [self removeToolTip: toolTipTag]; 484 toolTipTag = -1; 485 } 486 } 487 else 488 { 489 GSTTProvider *provider; 490 491 if (toolTipTag == -1) 492 { 493 NSRect rect; 494 495 rect = [view bounds]; 496 provider = [[GSTTProvider alloc] initWithObject: string 497 userData: nil]; 498 toolTipTag = [view addTrackingRect: rect 499 owner: self 500 userData: provider 501 assumeInside: NO]; 502 } 503 else 504 { 505 NSEnumerator *enumerator; 506 GSTrackingRect *rect; 507 508 enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; 509 while ((rect = [enumerator nextObject]) != nil) 510 { 511 if (rect->tag == toolTipTag && rect->owner == self) 512 { 513 [((GSTTProvider*)rect->user_data) setObject: string]; 514 } 515 } 516 } 517 } 518} 519 520- (NSString *) toolTip 521{ 522 NSEnumerator *enumerator; 523 GSTrackingRect *rect; 524 525 enumerator = [((NSViewPtr)view)->_tracking_rects objectEnumerator]; 526 while ((rect = [enumerator nextObject]) != nil) 527 { 528 if (rect->tag == toolTipTag) 529 { 530 return [((GSTTProvider*)rect->user_data) object]; 531 } 532 } 533 return nil; 534} 535 536@end 537 538@implementation GSToolTips (Private) 539 540- (void) _endDisplay 541{ 542 [self _endDisplay:NSNotFound]; 543} 544 545- (void) _endDisplay: (NSTrackingRectTag)tag 546{ 547 if (isOpening) 548 return; 549 if ([NSWindow _toolTipVisible] == self) 550 { 551 [NSWindow _setToolTipVisible: nil]; 552 } 553 /* If there is currently a timer running for this object and it is the target tag, 554 * cancel it. Always remove if the target tag is NSNotFound 555 */ 556 if (timer != nil && timedObject == self && (timedTag == tag || tag == NSNotFound)) 557 { 558 if ([timer isValid]) 559 { 560 [timer invalidate]; 561 } 562 timer = nil; 563 timedObject = nil; 564 timedTag = NSNotFound; 565 } 566 if (window != nil) 567 { 568 [window setFrame: NSZeroRect display: NO]; 569 [window orderOut:self]; 570 } 571 if (restoreMouseMoved == YES) 572 { 573 restoreMouseMoved = NO; 574 [[view window] setAcceptsMouseMovedEvents: NO]; 575 } 576} 577 578/* The delay timed out -- display the tooltip */ 579- (void) _timedOut: (NSTimer *)aTimer 580{ 581 CGFloat size; 582 NSString *toolTipString; 583 NSAttributedString *toolTipText = nil; 584 NSSize textSize; 585 NSPoint mouseLocation = [NSEvent mouseLocation]; 586 NSRect visible; 587 NSRect rect; 588 NSMutableDictionary *attributes; 589 590 // retain and autorelease the timer's userinfo because we 591 // may invalidate the timer (which releases the userinfo), 592 // but need the userinfo object to remain valid for the 593 // remainder of this method. 594 toolTipString = [[[aTimer userInfo] retain] autorelease]; 595 if ( (nil == toolTipString) || 596 ([toolTipString isEqualToString: @""]) ) 597 { 598 return; 599 } 600 601 if (timer != nil) 602 { 603 if ([timer isValid]) 604 { 605 [timer invalidate]; 606 } 607 timer = nil; 608 timedObject = nil; 609 timedTag = NSNotFound; 610 } 611 612 if ([window isVisible]) 613 { 614 /* Moved from one tooltip view to another ... so stop displaying 615 * the old tool tip before we start the new one. 616 * This is similar to the case in -mouseEntered: where we cancel 617 * the timer for one tooltip view because we have entered another 618 * one. 619 * To think about ... if we entered a tooltip rectangle without 620 * having left the previous one, then when we leave this rectangle 621 * we are probably back in the other one and should really restart 622 * the timer for the original view. However, this is a rare case 623 * so it's probably better to ignore it than add a lot of code to 624 * keep track of all entry and exit. 625 */ 626 [self _endDisplay]; 627 } 628 629 size = [[NSUserDefaults standardUserDefaults] 630 floatForKey: @"NSToolTipsFontSize"]; 631 632 if (size <= 0) 633 { 634 size = 10.0; 635 } 636 637 attributes = [NSMutableDictionary dictionary]; 638 [attributes setObject: [NSFont toolTipsFontOfSize: size] 639 forKey: NSFontAttributeName]; 640 [attributes setObject: [NSColor toolTipTextColor] 641 forKey: NSForegroundColorAttributeName]; 642 toolTipText = 643 [[NSAttributedString alloc] initWithString: toolTipString 644 attributes: attributes]; 645 textSize = [toolTipText size]; 646 if (textSize.width > 300) 647 { 648 NSRect rect; 649 rect = [toolTipText boundingRectWithSize: NSMakeSize(300, 1e7) 650 options: 0]; 651 textSize = rect.size; 652 // This extra pixel is needed, otherwise the last line gets cut off. 653 textSize.height += 1; 654 } 655 656 /* Create window just off the current mouse position 657 * Constrain it to be on screen, shrinking if necessary. 658 */ 659 rect = NSMakeRect(mouseLocation.x + 8, 660 mouseLocation.y - 16 - (textSize.height+3), 661 textSize.width + 4, textSize.height + 4); 662 visible = [[NSScreen mainScreen] visibleFrame]; 663 if (NSMaxY(rect) > NSMaxY(visible)) 664 { 665 rect.origin.y -= (NSMaxY(rect) - NSMaxY(visible)); 666 } 667 if (NSMinY(rect) < NSMinY(visible)) 668 { 669 rect.origin.y += (NSMinY(visible) - NSMinY(rect)); 670 } 671 if (NSMaxY(rect) > NSMaxY(visible)) 672 { 673 rect.origin.y = visible.origin.y; 674 rect.size.height = visible.size.height; 675 } 676 677 if (NSMaxX(rect) > NSMaxX(visible)) 678 { 679 rect.origin.x -= (NSMaxX(rect) - NSMaxX(visible)); 680 } 681 if (NSMinX(rect) < NSMinX(visible)) 682 { 683 rect.origin.x += (NSMinX(visible) - NSMinX(rect)); 684 } 685 if (NSMaxX(rect) > NSMaxX(visible)) 686 { 687 rect.origin.x = visible.origin.x; 688 rect.size.width = visible.size.width; 689 } 690 offset.height = rect.origin.y - mouseLocation.y; 691 offset.width = rect.origin.x - mouseLocation.x; 692 693 isOpening = YES; 694 [(GSTTView*)([window contentView]) setText: toolTipText]; 695 [window setFrame: rect display: NO]; 696 [window orderFront: nil]; 697 isOpening = NO; 698 699 RELEASE(toolTipText); 700} 701 702@end 703 704