Skip to content

Instantly share code, notes, and snippets.

@justingraves
Created September 3, 2009 08:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save justingraves/180177 to your computer and use it in GitHub Desktop.
Save justingraves/180177 to your computer and use it in GitHub Desktop.
A little rounded black tooltip you can easily point at stuff
/*
* CPTooltip.j
* Makes a little black rounded-rect gradient tooltip with some text (can have multiple lines with \n) which points at something.
* Usage example:
* tooltip = [[CPTooltip alloc] initWithText:@"Hello There" atPoint:CPPointMake(100,100)];
* [self addSubview:tooltip];
*
* The above will make a tooltip that says "Hello There" which is pointing at this view's 100x100 point.
* If the tooltip is too close to its superview's edges, it will adjust itself to maintain readability.
*
* Created by Justin Graves
* Copyright 2009, Infegy, Inc.
*
* 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.1 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; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import <AppKit/CPView.j>
@implementation CPTooltip : CPView
{
CPLabel _text;
CPPoint _target;
CPArray _clipPoints;
var _horizontalPadding;
var _verticalPadding;
CPPoint _arrowOffset;
BOOL _flippedArrow;
}
- (id)initWithText:(CPString)text atPoint:(CPPoint)target
{
if (self = [super initWithFrame:CGRectMake(0,0,26,26)])
{
if(!_text)
{
_text = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
[_text setFont:[CPFont boldSystemFontOfSize:11.0]];
[_text setTextColor:[CPColor colorWithCalibratedRed:1.0 green:1.0 blue:1.0 alpha:1.0]];
[_text setAlignment:CPCenterTextAlignment];
}
_target = target;
_arrowOffset = CPPointMakeZero();
_flippedArrow = NO;
[self setText:text];
[self addSubview:_text];
}
return self;
}
- (id)init
{
return [self initWithFrame:CGRectMakeZero()];
}
- (id)initWithFrame:(CGRect)aFrame
{
return [self initWithText:@"" atPoint:CPPointMakeZero()];
}
- (void)setText:(CPString)text
{
[self setText:text atPoint:_target];
}
- (void)setText:(CPString)text atPoint:(CPPoint)target
{
[_text setStringValue:text];
[_text sizeToFit];
// Make size around the requested point
var textSize = [_text frame].size;
// Padded height
_verticalPadding = 2;
textSize.height += (_verticalPadding * 2); // 20% padding top/bottom
// Padded width
_horizontalPadding = 5;
textSize.width += (_horizontalPadding * 2); // Add caps at the ends, each half of height
textSize.height += 6; // Add 6 pixel pointer
var lowerSegment = (textSize.width - 12 - (_horizontalPadding * 2)) / 2;
_clipPoints = Array(_horizontalPadding, _verticalPadding, lowerSegment);
[self setTarget:target withSize:textSize];
var textOrigin;
if(_flippedArrow)
textOrigin = CPPointMake(textSize.width/2, ((textSize.height - 6)/2) + 6);
else
textOrigin = CPPointMake(textSize.width/2, (textSize.height - 6)/2);
[_text setCenter:textOrigin];
}
- (void)setTarget:(CPPoint)target withSize:(CPSize)size
{
var bounds = [self frame];
var sizesMatch = bounds.size.width == size.width && bounds.size.height == size.height;
if(target.x == _target.x && target.y == _target.y && sizesMatch)
return;
_target.x = target.x;
_target.y = target.y;
// Find origin based on target point.
bounds.origin.y = target.y - size.height;
bounds.origin.x = target.x - size.width/2;
bounds.size.width = size.width;
bounds.size.height = size.height;
// Check if the tooltip is too close to the superview's edges
// If it is, move it around so the text is still clearly visibile
var oldOffset = CPPointMake(_arrowOffset.x, _arrowOffset.y);
var wasFlipped = _flippedArrow;
_arrowOffset.x = 0; _arrowOffset.y = 0;
_flippedArrow = NO;
if(_superview)
{
// Handle getting pushed to superview's edges
if(bounds.origin.x + _horizontalPadding < 0)
{
bounds.origin.x = -_horizontalPadding;
_arrowOffset.x = target.x - (bounds.origin.x + (size.width/2));
}
else if(bounds.origin.x + size.width - _horizontalPadding > [_superview bounds].size.width)
{
bounds.origin.x -= (bounds.origin.x + size.width - _horizontalPadding) - [_superview bounds].size.width;
_arrowOffset.x = target.x - (bounds.origin.x + (size.width/2));
}
// Handle moving too high up
if(bounds.origin.y < 0)
{
_flippedArrow = YES;
bounds.origin.y = target.y;
}
}
// If the arrow offset changed and sizes match, we need to redraw and setFrame didn't do it
// If sizes don't match, setFrame (just below) will redraw for us
needsDraw = NO;
if((oldOffset.x != _arrowOffset.x || oldOffset.y != _arrowOffset.y || wasFlipped != _flippedArrow) && sizesMatch)
needsDraw = YES;
if(needsDraw)
{
[self setFrameOrigin:bounds.origin];
[self setNeedsLayout];
[self setNeedsDisplay:YES];
}
else
[self setFrame:bounds];
}
- (void)drawRect:(CPRect)aRect
{
var context = [[CPGraphicsContext currentContext] graphicsPort];
CGContextBeginPath(context);
CGContextSaveGState(context);
// Prepare gradient for background
var locations = new Array(0.0, 1.5/_bounds.size.height, 1.0);
var components = new Array( 0.9, 0.9, 0.9, 1.0, // Start (top) color
0.3, 0.3, 0.3, 0.80,
0.0, 0.0, 0.0, 0.80); // End (bottom) color
var myGradient = CGGradientCreateWithColorComponents(CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), components, locations, 3);
var myStartPoint = CPPointMake(0,0);
var myEndPoint = CPPointMake(0,_bounds.size.height);
// Draw the path for the main body: a rounded rect
var radius = 6;
var rect = CPRectMake(_bounds.origin.x, _bounds.origin.y, _bounds.size.width, _bounds.size.height);
rect.size.height -= 6;
// Draws the rect by setting up scaled four arcs
CGContextSaveGState(context);
if(_flippedArrow)
CGContextTranslateCTM(context, 0, 6);
CGContextScaleCTM (context, radius, radius);
fw = CGRectGetWidth (rect) / radius;
fh = CGRectGetHeight (rect) / radius;
CGContextMoveToPoint(context, fw, fh/2);
CGContextAddArcToPoint(context, fw, fh, fw/2, fh, 1);
CGContextAddArcToPoint(context, 0, fh, 0, fh/2, 1);
CGContextAddArcToPoint(context, 0, 0, fw/2, 0, 1);
CGContextAddArcToPoint(context, fw, 0, fw, fh/2, 1);
CGContextClosePath(context);
CGContextRestoreGState(context);
// Arrow pointing to target
// If the tooltip is too close to the top of its containing view, the arrow points up as the tooltip is moved down
if(_flippedArrow)
{
CGContextMoveToPoint(context, _clipPoints[0] + _clipPoints[2] + _arrowOffset.x, 6);
CGContextAddLineToPoint(context, _clipPoints[0] + _clipPoints[2] + 12 + _arrowOffset.x, 6);
CGContextAddLineToPoint(context, _clipPoints[0] + _clipPoints[2] + 6 + _arrowOffset.x, 0);
}
// Otherwise, the arrow points down
else
{
CGContextMoveToPoint(context, _clipPoints[0] + _clipPoints[2] + _arrowOffset.x, rect.size.height);
CGContextAddLineToPoint(context, _clipPoints[0] + _clipPoints[2] + 12 + _arrowOffset.x, rect.size.height);
CGContextAddLineToPoint(context, _clipPoints[0] + _clipPoints[2] + 6 + _arrowOffset.x, rect.size.height + 6);
}
CGContextClosePath(context);
// Actual gradient fill operation
CGContextClip(context);
CGContextDrawLinearGradient(context, myGradient, myStartPoint, myEndPoint, 0);
CGContextRestoreGState(context);
[super drawRect:aRect];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment