952 lines
29 KiB
Objective-C
952 lines
29 KiB
Objective-C
//
|
|
// MAAttachedWindow.m
|
|
//
|
|
// Created by Matt Gemmell on 27/09/2007.
|
|
// Copyright 2007 Magic Aubergine.
|
|
//
|
|
|
|
#import "MAAttachedWindow.h"
|
|
|
|
#define MAATTACHEDWINDOW_DEFAULT_BACKGROUND_COLOR [NSColor colorWithCalibratedWhite:0.1 alpha:0.75]
|
|
#define MAATTACHEDWINDOW_DEFAULT_BORDER_COLOR [NSColor whiteColor]
|
|
#define MAATTACHEDWINDOW_SCALE_FACTOR [[NSScreen mainScreen] userSpaceScaleFactor]
|
|
|
|
@interface MAAttachedWindow (MAPrivateMethods)
|
|
|
|
// Geometry
|
|
- (void)_updateGeometry;
|
|
- (MAWindowPosition)_bestSideForAutomaticPosition;
|
|
- (float)_arrowInset;
|
|
|
|
// Drawing
|
|
- (void)_updateBackground;
|
|
- (NSColor *)_backgroundColorPatternImage;
|
|
- (NSBezierPath *)_backgroundPath;
|
|
- (void)_appendArrowToPath:(NSBezierPath *)path;
|
|
- (void)_redisplay;
|
|
|
|
@end
|
|
|
|
@implementation MAAttachedWindow
|
|
|
|
|
|
#pragma mark Initializers
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
inWindow:(NSWindow *)window
|
|
onSide:(MAWindowPosition)side
|
|
atDistance:(float)distance
|
|
{
|
|
// Insist on having a valid view.
|
|
if (!view) {
|
|
return nil;
|
|
}
|
|
|
|
// Create dummy initial contentRect for window.
|
|
NSRect contentRect = NSZeroRect;
|
|
contentRect.size = [view frame].size;
|
|
|
|
if ((self = [super initWithContentRect:contentRect
|
|
styleMask:NSBorderlessWindowMask
|
|
backing:NSBackingStoreBuffered
|
|
defer:NO])) {
|
|
_view = view;
|
|
_window = window;
|
|
_point = point;
|
|
_side = side;
|
|
_distance = distance;
|
|
|
|
// Configure window characteristics.
|
|
[super setBackgroundColor:[NSColor clearColor]];
|
|
[self setMovableByWindowBackground:NO];
|
|
[self setExcludedFromWindowsMenu:YES];
|
|
[self setAlphaValue:1.0];
|
|
[self setOpaque:NO];
|
|
[self setHasShadow:YES];
|
|
[self useOptimizedDrawing:YES];
|
|
|
|
// Set up some sensible defaults for display.
|
|
_MABackgroundColor = [MAATTACHEDWINDOW_DEFAULT_BACKGROUND_COLOR copy];
|
|
borderColor = [MAATTACHEDWINDOW_DEFAULT_BORDER_COLOR copy];
|
|
borderWidth = 2.0;
|
|
viewMargin = 2.0;
|
|
arrowBaseWidth = 20.0;
|
|
arrowHeight = 16.0;
|
|
hasArrow = YES;
|
|
cornerRadius = 8.0;
|
|
drawsRoundCornerBesideArrow = YES;
|
|
_resizing = NO;
|
|
|
|
// Work out what side to put the window on if it's "automatic".
|
|
if (_side == MAPositionAutomatic) {
|
|
_side = [self _bestSideForAutomaticPosition];
|
|
}
|
|
|
|
// Configure our initial geometry.
|
|
[self _updateGeometry];
|
|
|
|
// Update the background.
|
|
[self _updateBackground];
|
|
|
|
// Add view as subview of our contentView.
|
|
[[self contentView] addSubview:_view];
|
|
|
|
// Subscribe to notifications for when we change size.
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
|
selector:@selector(windowDidResize:)
|
|
name:NSWindowDidResizeNotification
|
|
object:self];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
inWindow:(NSWindow *)window
|
|
atDistance:(float)distance
|
|
{
|
|
return [self initWithView:view attachedToPoint:point
|
|
inWindow:window onSide:MAPositionAutomatic
|
|
atDistance:distance];
|
|
}
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
onSide:(MAWindowPosition)side
|
|
atDistance:(float)distance
|
|
{
|
|
return [self initWithView:view attachedToPoint:point
|
|
inWindow:nil onSide:side
|
|
atDistance:distance];
|
|
}
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
atDistance:(float)distance
|
|
{
|
|
return [self initWithView:view attachedToPoint:point
|
|
inWindow:nil onSide:MAPositionAutomatic
|
|
atDistance:distance];
|
|
}
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
inWindow:(NSWindow *)window
|
|
{
|
|
return [self initWithView:view attachedToPoint:point
|
|
inWindow:window onSide:MAPositionAutomatic
|
|
atDistance:0];
|
|
}
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
onSide:(MAWindowPosition)side
|
|
{
|
|
return [self initWithView:view attachedToPoint:point
|
|
inWindow:nil onSide:side
|
|
atDistance:0];
|
|
}
|
|
|
|
|
|
- (MAAttachedWindow *)initWithView:(NSView *)view
|
|
attachedToPoint:(NSPoint)point
|
|
{
|
|
return [self initWithView:view attachedToPoint:point
|
|
inWindow:nil onSide:MAPositionAutomatic
|
|
atDistance:0];
|
|
}
|
|
|
|
|
|
- (void)dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
[borderColor release];
|
|
[_MABackgroundColor release];
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
|
|
#pragma mark Geometry
|
|
|
|
|
|
- (void)_updateGeometry
|
|
{
|
|
NSRect contentRect = NSZeroRect;
|
|
contentRect.size = [_view frame].size;
|
|
|
|
// Account for viewMargin.
|
|
_viewFrame = NSMakeRect(viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR,
|
|
viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR,
|
|
[_view frame].size.width, [_view frame].size.height);
|
|
contentRect = NSInsetRect(contentRect,
|
|
-viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR,
|
|
-viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR);
|
|
|
|
// Account for arrowHeight in new window frame.
|
|
// Note: we always leave room for the arrow, even if it currently set to
|
|
// not be shown. This is so it can easily be toggled whilst the window
|
|
// is visible, without altering the window's frame origin point.
|
|
float scaledArrowHeight = arrowHeight * MAATTACHEDWINDOW_SCALE_FACTOR;
|
|
switch (_side) {
|
|
case MAPositionLeft:
|
|
case MAPositionLeftTop:
|
|
case MAPositionLeftBottom:
|
|
contentRect.size.width += scaledArrowHeight;
|
|
break;
|
|
case MAPositionRight:
|
|
case MAPositionRightTop:
|
|
case MAPositionRightBottom:
|
|
_viewFrame.origin.x += scaledArrowHeight;
|
|
contentRect.size.width += scaledArrowHeight;
|
|
break;
|
|
case MAPositionTop:
|
|
case MAPositionTopLeft:
|
|
case MAPositionTopRight:
|
|
_viewFrame.origin.y += scaledArrowHeight;
|
|
contentRect.size.height += scaledArrowHeight;
|
|
break;
|
|
case MAPositionBottom:
|
|
case MAPositionBottomLeft:
|
|
case MAPositionBottomRight:
|
|
contentRect.size.height += scaledArrowHeight;
|
|
break;
|
|
default:
|
|
break; // won't happen, but this satisfies gcc with -Wall
|
|
}
|
|
|
|
// Position frame origin appropriately for _side, accounting for arrow-inset.
|
|
contentRect.origin = (_window) ? [_window convertBaseToScreen:_point] : _point;
|
|
float arrowInset = [self _arrowInset];
|
|
float halfWidth = contentRect.size.width / 2.0;
|
|
float halfHeight = contentRect.size.height / 2.0;
|
|
switch (_side) {
|
|
case MAPositionTopLeft:
|
|
contentRect.origin.x -= contentRect.size.width - arrowInset;
|
|
break;
|
|
case MAPositionTop:
|
|
contentRect.origin.x -= halfWidth;
|
|
break;
|
|
case MAPositionTopRight:
|
|
contentRect.origin.x -= arrowInset;
|
|
break;
|
|
case MAPositionBottomLeft:
|
|
contentRect.origin.y -= contentRect.size.height;
|
|
contentRect.origin.x -= contentRect.size.width - arrowInset;
|
|
break;
|
|
case MAPositionBottom:
|
|
contentRect.origin.y -= contentRect.size.height;
|
|
contentRect.origin.x -= halfWidth;
|
|
break;
|
|
case MAPositionBottomRight:
|
|
contentRect.origin.x -= arrowInset;
|
|
contentRect.origin.y -= contentRect.size.height;
|
|
break;
|
|
case MAPositionLeftTop:
|
|
contentRect.origin.x -= contentRect.size.width;
|
|
contentRect.origin.y -= arrowInset;
|
|
break;
|
|
case MAPositionLeft:
|
|
contentRect.origin.x -= contentRect.size.width;
|
|
contentRect.origin.y -= halfHeight;
|
|
break;
|
|
case MAPositionLeftBottom:
|
|
contentRect.origin.x -= contentRect.size.width;
|
|
contentRect.origin.y -= contentRect.size.height - arrowInset;
|
|
break;
|
|
case MAPositionRightTop:
|
|
contentRect.origin.y -= arrowInset;
|
|
break;
|
|
case MAPositionRight:
|
|
contentRect.origin.y -= halfHeight;
|
|
break;
|
|
case MAPositionRightBottom:
|
|
contentRect.origin.y -= contentRect.size.height - arrowInset;
|
|
break;
|
|
default:
|
|
break; // won't happen, but this satisfies gcc with -Wall
|
|
}
|
|
|
|
// Account for _distance in new window frame.
|
|
switch (_side) {
|
|
case MAPositionLeft:
|
|
case MAPositionLeftTop:
|
|
case MAPositionLeftBottom:
|
|
contentRect.origin.x -= _distance;
|
|
break;
|
|
case MAPositionRight:
|
|
case MAPositionRightTop:
|
|
case MAPositionRightBottom:
|
|
contentRect.origin.x += _distance;
|
|
break;
|
|
case MAPositionTop:
|
|
case MAPositionTopLeft:
|
|
case MAPositionTopRight:
|
|
contentRect.origin.y += _distance;
|
|
break;
|
|
case MAPositionBottom:
|
|
case MAPositionBottomLeft:
|
|
case MAPositionBottomRight:
|
|
contentRect.origin.y -= _distance;
|
|
break;
|
|
default:
|
|
break; // won't happen, but this satisfies gcc with -Wall
|
|
}
|
|
|
|
// Reconfigure window and view frames appropriately.
|
|
[self setFrame:contentRect display:NO];
|
|
[_view setFrame:_viewFrame];
|
|
}
|
|
|
|
|
|
- (MAWindowPosition)_bestSideForAutomaticPosition
|
|
{
|
|
// Get all relevant geometry in screen coordinates.
|
|
NSRect screenFrame;
|
|
if (_window && [_window screen]) {
|
|
screenFrame = [[_window screen] visibleFrame];
|
|
} else {
|
|
screenFrame = [[NSScreen mainScreen] visibleFrame];
|
|
}
|
|
NSPoint pointOnScreen = (_window) ? [_window convertBaseToScreen:_point] : _point;
|
|
NSSize viewSize = [_view frame].size;
|
|
viewSize.width += (viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR) * 2.0;
|
|
viewSize.height += (viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR) * 2.0;
|
|
MAWindowPosition side = MAPositionBottom; // By default, position us centered below.
|
|
float scaledArrowHeight = (arrowHeight * MAATTACHEDWINDOW_SCALE_FACTOR) + _distance;
|
|
|
|
// We'd like to display directly below the specified point, since this gives a
|
|
// sense of a relationship between the point and this window. Check there's room.
|
|
if (pointOnScreen.y - viewSize.height - scaledArrowHeight < NSMinY(screenFrame)) {
|
|
// We'd go off the bottom of the screen. Try the right.
|
|
if (pointOnScreen.x + viewSize.width + scaledArrowHeight >= NSMaxX(screenFrame)) {
|
|
// We'd go off the right of the screen. Try the left.
|
|
if (pointOnScreen.x - viewSize.width - scaledArrowHeight < NSMinX(screenFrame)) {
|
|
// We'd go off the left of the screen. Try the top.
|
|
if (pointOnScreen.y + viewSize.height + scaledArrowHeight < NSMaxY(screenFrame)) {
|
|
side = MAPositionTop;
|
|
}
|
|
} else {
|
|
side = MAPositionLeft;
|
|
}
|
|
} else {
|
|
side = MAPositionRight;
|
|
}
|
|
}
|
|
|
|
float halfWidth = viewSize.width / 2.0;
|
|
float halfHeight = viewSize.height / 2.0;
|
|
|
|
NSRect parentFrame = (_window) ? [_window frame] : screenFrame;
|
|
float arrowInset = [self _arrowInset];
|
|
|
|
// We're currently at a primary side.
|
|
// Try to avoid going outwith the parent area in the secondary dimension,
|
|
// by checking to see if an appropriate corner side would be better.
|
|
switch (side) {
|
|
case MAPositionBottom:
|
|
case MAPositionTop:
|
|
// Check to see if we go beyond the left edge of the parent area.
|
|
if (pointOnScreen.x - halfWidth < NSMinX(parentFrame)) {
|
|
// We go beyond the left edge. Try using right position.
|
|
if (pointOnScreen.x + viewSize.width - arrowInset < NSMaxX(screenFrame)) {
|
|
// We'd still be on-screen using right, so use it.
|
|
if (side == MAPositionBottom) {
|
|
side = MAPositionBottomRight;
|
|
} else {
|
|
side = MAPositionTopRight;
|
|
}
|
|
}
|
|
} else if (pointOnScreen.x + halfWidth >= NSMaxX(parentFrame)) {
|
|
// We go beyond the right edge. Try using left position.
|
|
if (pointOnScreen.x - viewSize.width + arrowInset >= NSMinX(screenFrame)) {
|
|
// We'd still be on-screen using left, so use it.
|
|
if (side == MAPositionBottom) {
|
|
side = MAPositionBottomLeft;
|
|
} else {
|
|
side = MAPositionTopLeft;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case MAPositionRight:
|
|
case MAPositionLeft:
|
|
// Check to see if we go beyond the bottom edge of the parent area.
|
|
if (pointOnScreen.y - halfHeight < NSMinY(parentFrame)) {
|
|
// We go beyond the bottom edge. Try using top position.
|
|
if (pointOnScreen.y + viewSize.height - arrowInset < NSMaxY(screenFrame)) {
|
|
// We'd still be on-screen using top, so use it.
|
|
if (side == MAPositionRight) {
|
|
side = MAPositionRightTop;
|
|
} else {
|
|
side = MAPositionLeftTop;
|
|
}
|
|
}
|
|
} else if (pointOnScreen.y + halfHeight >= NSMaxY(parentFrame)) {
|
|
// We go beyond the top edge. Try using bottom position.
|
|
if (pointOnScreen.y - viewSize.height + arrowInset >= NSMinY(screenFrame)) {
|
|
// We'd still be on-screen using bottom, so use it.
|
|
if (side == MAPositionRight) {
|
|
side = MAPositionRightBottom;
|
|
} else {
|
|
side = MAPositionLeftBottom;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break; // won't happen, but this satisfies gcc with -Wall
|
|
}
|
|
|
|
return side;
|
|
}
|
|
|
|
|
|
- (float)_arrowInset
|
|
{
|
|
float cornerInset = (drawsRoundCornerBesideArrow) ? cornerRadius : 0;
|
|
return (cornerInset + (arrowBaseWidth / 2.0)) * MAATTACHEDWINDOW_SCALE_FACTOR;
|
|
}
|
|
|
|
|
|
#pragma mark Drawing
|
|
|
|
|
|
- (void)_updateBackground
|
|
{
|
|
// Call NSWindow's implementation of -setBackgroundColor: because we override
|
|
// it in this class to let us set the entire background image of the window
|
|
// as an NSColor patternImage.
|
|
NSDisableScreenUpdates();
|
|
[super setBackgroundColor:[self _backgroundColorPatternImage]];
|
|
if ([self isVisible]) {
|
|
[self display];
|
|
[self invalidateShadow];
|
|
}
|
|
NSEnableScreenUpdates();
|
|
}
|
|
|
|
|
|
- (NSColor *)_backgroundColorPatternImage
|
|
{
|
|
NSImage *bg = [[NSImage alloc] initWithSize:[self frame].size];
|
|
NSRect bgRect = NSZeroRect;
|
|
bgRect.size = [bg size];
|
|
|
|
[bg lockFocus];
|
|
NSBezierPath *bgPath = [self _backgroundPath];
|
|
[NSGraphicsContext saveGraphicsState];
|
|
[bgPath addClip];
|
|
|
|
// Draw background.
|
|
[_MABackgroundColor set];
|
|
[bgPath fill];
|
|
|
|
// Draw border if appropriate.
|
|
if (borderWidth > 0) {
|
|
// Double the borderWidth since we're drawing inside the path.
|
|
[bgPath setLineWidth:(borderWidth * 2.0) * MAATTACHEDWINDOW_SCALE_FACTOR];
|
|
[borderColor set];
|
|
[bgPath stroke];
|
|
}
|
|
|
|
[NSGraphicsContext restoreGraphicsState];
|
|
[bg unlockFocus];
|
|
|
|
return [NSColor colorWithPatternImage:[bg autorelease]];
|
|
}
|
|
|
|
|
|
- (NSBezierPath *)_backgroundPath
|
|
{
|
|
/*
|
|
Construct path for window background, taking account of:
|
|
1. hasArrow
|
|
2. _side
|
|
3. drawsRoundCornerBesideArrow
|
|
4. arrowBaseWidth
|
|
5. arrowHeight
|
|
6. cornerRadius
|
|
*/
|
|
|
|
float scaleFactor = MAATTACHEDWINDOW_SCALE_FACTOR;
|
|
float scaledRadius = cornerRadius * scaleFactor;
|
|
float scaledArrowWidth = arrowBaseWidth * scaleFactor;
|
|
float halfArrowWidth = scaledArrowWidth / 2.0;
|
|
NSRect contentArea = NSInsetRect(_viewFrame,
|
|
-viewMargin * scaleFactor,
|
|
-viewMargin * scaleFactor);
|
|
float minX = ceilf(NSMinX(contentArea) * scaleFactor + 0.5f);
|
|
float midX = NSMidX(contentArea) * scaleFactor;
|
|
float maxX = floorf(NSMaxX(contentArea) * scaleFactor - 0.5f);
|
|
float minY = ceilf(NSMinY(contentArea) * scaleFactor + 0.5f);
|
|
float midY = NSMidY(contentArea) * scaleFactor;
|
|
float maxY = floorf(NSMaxY(contentArea) * scaleFactor - 0.5f);
|
|
|
|
NSBezierPath *path = [NSBezierPath bezierPath];
|
|
[path setLineJoinStyle:NSRoundLineJoinStyle];
|
|
|
|
// Begin at top-left. This will be either after the rounded corner, or
|
|
// at the top-left point if cornerRadius is zero and/or we're drawing
|
|
// the arrow at the top-left or left-top without a rounded corner.
|
|
NSPoint currPt = NSMakePoint(minX, maxY);
|
|
if (scaledRadius > 0 &&
|
|
(drawsRoundCornerBesideArrow ||
|
|
(_side != MAPositionBottomRight && _side != MAPositionRightBottom))
|
|
) {
|
|
currPt.x += scaledRadius;
|
|
}
|
|
|
|
NSPoint endOfLine = NSMakePoint(maxX, maxY);
|
|
BOOL shouldDrawNextCorner = NO;
|
|
if (scaledRadius > 0 &&
|
|
(drawsRoundCornerBesideArrow ||
|
|
(_side != MAPositionBottomLeft && _side != MAPositionLeftBottom))
|
|
) {
|
|
endOfLine.x -= scaledRadius;
|
|
shouldDrawNextCorner = YES;
|
|
}
|
|
|
|
[path moveToPoint:currPt];
|
|
|
|
// If arrow should be drawn at top-left point, draw it.
|
|
if (_side == MAPositionBottomRight) {
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionBottom) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(midX - halfArrowWidth, maxY)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionBottomLeft) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(endOfLine.x - scaledArrowWidth, maxY)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
}
|
|
|
|
// Line to end of this side.
|
|
[path lineToPoint:endOfLine];
|
|
|
|
// Rounded corner on top-right.
|
|
if (shouldDrawNextCorner) {
|
|
[path appendBezierPathWithArcFromPoint:NSMakePoint(maxX, maxY)
|
|
toPoint:NSMakePoint(maxX, maxY - scaledRadius)
|
|
radius:scaledRadius];
|
|
}
|
|
|
|
|
|
// Draw the right side, beginning at the top-right.
|
|
endOfLine = NSMakePoint(maxX, minY);
|
|
shouldDrawNextCorner = NO;
|
|
if (scaledRadius > 0 &&
|
|
(drawsRoundCornerBesideArrow ||
|
|
(_side != MAPositionTopLeft && _side != MAPositionLeftTop))
|
|
) {
|
|
endOfLine.y += scaledRadius;
|
|
shouldDrawNextCorner = YES;
|
|
}
|
|
|
|
// If arrow should be drawn at right-top point, draw it.
|
|
if (_side == MAPositionLeftBottom) {
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionLeft) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(maxX, midY + halfArrowWidth)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionLeftTop) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(maxX, endOfLine.y + scaledArrowWidth)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
}
|
|
|
|
// Line to end of this side.
|
|
[path lineToPoint:endOfLine];
|
|
|
|
// Rounded corner on bottom-right.
|
|
if (shouldDrawNextCorner) {
|
|
[path appendBezierPathWithArcFromPoint:NSMakePoint(maxX, minY)
|
|
toPoint:NSMakePoint(maxX - scaledRadius, minY)
|
|
radius:scaledRadius];
|
|
}
|
|
|
|
|
|
// Draw the bottom side, beginning at the bottom-right.
|
|
endOfLine = NSMakePoint(minX, minY);
|
|
shouldDrawNextCorner = NO;
|
|
if (scaledRadius > 0 &&
|
|
(drawsRoundCornerBesideArrow ||
|
|
(_side != MAPositionTopRight && _side != MAPositionRightTop))
|
|
) {
|
|
endOfLine.x += scaledRadius;
|
|
shouldDrawNextCorner = YES;
|
|
}
|
|
|
|
// If arrow should be drawn at bottom-right point, draw it.
|
|
if (_side == MAPositionTopLeft) {
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionTop) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(midX + halfArrowWidth, minY)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionTopRight) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(endOfLine.x + scaledArrowWidth, minY)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
}
|
|
|
|
// Line to end of this side.
|
|
[path lineToPoint:endOfLine];
|
|
|
|
// Rounded corner on bottom-left.
|
|
if (shouldDrawNextCorner) {
|
|
[path appendBezierPathWithArcFromPoint:NSMakePoint(minX, minY)
|
|
toPoint:NSMakePoint(minX, minY + scaledRadius)
|
|
radius:scaledRadius];
|
|
}
|
|
|
|
|
|
// Draw the left side, beginning at the bottom-left.
|
|
endOfLine = NSMakePoint(minX, maxY);
|
|
shouldDrawNextCorner = NO;
|
|
if (scaledRadius > 0 &&
|
|
(drawsRoundCornerBesideArrow ||
|
|
(_side != MAPositionRightBottom && _side != MAPositionBottomRight))
|
|
) {
|
|
endOfLine.y -= scaledRadius;
|
|
shouldDrawNextCorner = YES;
|
|
}
|
|
|
|
// If arrow should be drawn at left-bottom point, draw it.
|
|
if (_side == MAPositionRightTop) {
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionRight) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(minX, midY - halfArrowWidth)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
} else if (_side == MAPositionRightBottom) {
|
|
// Line to relevant point before arrow.
|
|
[path lineToPoint:NSMakePoint(minX, endOfLine.y - scaledArrowWidth)];
|
|
// Draw arrow.
|
|
[self _appendArrowToPath:path];
|
|
}
|
|
|
|
// Line to end of this side.
|
|
[path lineToPoint:endOfLine];
|
|
|
|
// Rounded corner on top-left.
|
|
if (shouldDrawNextCorner) {
|
|
[path appendBezierPathWithArcFromPoint:NSMakePoint(minX, maxY)
|
|
toPoint:NSMakePoint(minX + scaledRadius, maxY)
|
|
radius:scaledRadius];
|
|
}
|
|
|
|
[path closePath];
|
|
return path;
|
|
}
|
|
|
|
|
|
- (void)_appendArrowToPath:(NSBezierPath *)path
|
|
{
|
|
if (!hasArrow) {
|
|
return;
|
|
}
|
|
|
|
float scaleFactor = MAATTACHEDWINDOW_SCALE_FACTOR;
|
|
float scaledArrowWidth = arrowBaseWidth * scaleFactor;
|
|
float halfArrowWidth = scaledArrowWidth / 2.0;
|
|
float scaledArrowHeight = arrowHeight * scaleFactor;
|
|
NSPoint currPt = [path currentPoint];
|
|
NSPoint tipPt = currPt;
|
|
NSPoint endPt = currPt;
|
|
|
|
// Note: we always build the arrow path in a clockwise direction.
|
|
switch (_side) {
|
|
case MAPositionLeft:
|
|
case MAPositionLeftTop:
|
|
case MAPositionLeftBottom:
|
|
// Arrow points towards right. We're starting from the top.
|
|
tipPt.x += scaledArrowHeight;
|
|
tipPt.y -= halfArrowWidth;
|
|
endPt.y -= scaledArrowWidth;
|
|
break;
|
|
case MAPositionRight:
|
|
case MAPositionRightTop:
|
|
case MAPositionRightBottom:
|
|
// Arrow points towards left. We're starting from the bottom.
|
|
tipPt.x -= scaledArrowHeight;
|
|
tipPt.y += halfArrowWidth;
|
|
endPt.y += scaledArrowWidth;
|
|
break;
|
|
case MAPositionTop:
|
|
case MAPositionTopLeft:
|
|
case MAPositionTopRight:
|
|
// Arrow points towards bottom. We're starting from the right.
|
|
tipPt.y -= scaledArrowHeight;
|
|
tipPt.x -= halfArrowWidth;
|
|
endPt.x -= scaledArrowWidth;
|
|
break;
|
|
case MAPositionBottom:
|
|
case MAPositionBottomLeft:
|
|
case MAPositionBottomRight:
|
|
// Arrow points towards top. We're starting from the left.
|
|
tipPt.y += scaledArrowHeight;
|
|
tipPt.x += halfArrowWidth;
|
|
endPt.x += scaledArrowWidth;
|
|
break;
|
|
default:
|
|
break; // won't happen, but this satisfies gcc with -Wall
|
|
}
|
|
|
|
[path lineToPoint:tipPt];
|
|
[path lineToPoint:endPt];
|
|
}
|
|
|
|
|
|
- (void)_redisplay
|
|
{
|
|
if (_resizing) {
|
|
return;
|
|
}
|
|
|
|
_resizing = YES;
|
|
NSDisableScreenUpdates();
|
|
[self _updateGeometry];
|
|
[self _updateBackground];
|
|
NSEnableScreenUpdates();
|
|
_resizing = NO;
|
|
}
|
|
|
|
|
|
# pragma mark Window Behaviour
|
|
|
|
|
|
- (BOOL)canBecomeMainWindow
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
|
|
- (BOOL)canBecomeKeyWindow
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL)isExcludedFromWindowsMenu
|
|
{
|
|
return YES;
|
|
}
|
|
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem *)item
|
|
{
|
|
if (_window) {
|
|
return [_window validateMenuItem:item];
|
|
}
|
|
return [super validateMenuItem:item];
|
|
}
|
|
|
|
|
|
- (IBAction)performClose:(id)sender
|
|
{
|
|
if (_window) {
|
|
[_window performClose:sender];
|
|
} else {
|
|
[super performClose:sender];
|
|
}
|
|
}
|
|
|
|
|
|
# pragma mark Notification handlers
|
|
|
|
|
|
- (void)windowDidResize:(NSNotification *)note
|
|
{
|
|
[self _redisplay];
|
|
}
|
|
|
|
|
|
#pragma mark Accessors
|
|
|
|
|
|
- (void)setPoint:(NSPoint)point side:(MAWindowPosition)side
|
|
{
|
|
// Thanks to Martin Redington.
|
|
_point = point;
|
|
_side = side;
|
|
NSDisableScreenUpdates();
|
|
[self _updateGeometry];
|
|
[self _updateBackground];
|
|
NSEnableScreenUpdates();
|
|
}
|
|
|
|
|
|
- (NSColor *)windowBackgroundColor {
|
|
return [[_MABackgroundColor retain] autorelease];
|
|
}
|
|
|
|
|
|
- (void)setBackgroundColor:(NSColor *)value {
|
|
if (_MABackgroundColor != value) {
|
|
[_MABackgroundColor release];
|
|
_MABackgroundColor = [value copy];
|
|
|
|
[self _updateBackground];
|
|
}
|
|
}
|
|
|
|
|
|
- (NSColor *)borderColor {
|
|
return [[borderColor retain] autorelease];
|
|
}
|
|
|
|
|
|
- (void)setBorderColor:(NSColor *)value {
|
|
if (borderColor != value) {
|
|
[borderColor release];
|
|
borderColor = [value copy];
|
|
|
|
[self _updateBackground];
|
|
}
|
|
}
|
|
|
|
|
|
- (float)borderWidth {
|
|
return borderWidth;
|
|
}
|
|
|
|
|
|
- (void)setBorderWidth:(float)value {
|
|
if (borderWidth != value) {
|
|
float maxBorderWidth = viewMargin;
|
|
if (value <= maxBorderWidth) {
|
|
borderWidth = value;
|
|
} else {
|
|
borderWidth = maxBorderWidth;
|
|
}
|
|
|
|
[self _updateBackground];
|
|
}
|
|
}
|
|
|
|
|
|
- (float)viewMargin {
|
|
return viewMargin;
|
|
}
|
|
|
|
|
|
- (void)setViewMargin:(float)value {
|
|
if (viewMargin != value) {
|
|
viewMargin = MAX(value, 0.0);
|
|
|
|
// Adjust cornerRadius appropriately (which will also adjust arrowBaseWidth).
|
|
[self setCornerRadius:cornerRadius];
|
|
}
|
|
}
|
|
|
|
|
|
- (float)arrowBaseWidth {
|
|
return arrowBaseWidth;
|
|
}
|
|
|
|
|
|
- (void)setArrowBaseWidth:(float)value {
|
|
float maxWidth = (MIN(_viewFrame.size.width, _viewFrame.size.height) +
|
|
(viewMargin * 2.0)) - cornerRadius;
|
|
if (drawsRoundCornerBesideArrow) {
|
|
maxWidth -= cornerRadius;
|
|
}
|
|
if (value <= maxWidth) {
|
|
arrowBaseWidth = value;
|
|
} else {
|
|
arrowBaseWidth = maxWidth;
|
|
}
|
|
|
|
[self _redisplay];
|
|
}
|
|
|
|
|
|
- (float)arrowHeight {
|
|
return arrowHeight;
|
|
}
|
|
|
|
|
|
- (void)setArrowHeight:(float)value {
|
|
if (arrowHeight != value) {
|
|
arrowHeight = value;
|
|
|
|
[self _redisplay];
|
|
}
|
|
}
|
|
|
|
|
|
- (float)hasArrow {
|
|
return hasArrow;
|
|
}
|
|
|
|
|
|
- (void)setHasArrow:(float)value {
|
|
if (hasArrow != value) {
|
|
hasArrow = value;
|
|
|
|
[self _updateBackground];
|
|
}
|
|
}
|
|
|
|
|
|
- (float)cornerRadius {
|
|
return cornerRadius;
|
|
}
|
|
|
|
|
|
- (void)setCornerRadius:(float)value {
|
|
float maxRadius = ((MIN(_viewFrame.size.width, _viewFrame.size.height) +
|
|
(viewMargin * 2.0)) - arrowBaseWidth) / 2.0;
|
|
if (value <= maxRadius) {
|
|
cornerRadius = value;
|
|
} else {
|
|
cornerRadius = maxRadius;
|
|
}
|
|
cornerRadius = MAX(cornerRadius, 0.0);
|
|
|
|
// Adjust arrowBaseWidth appropriately.
|
|
[self setArrowBaseWidth:arrowBaseWidth];
|
|
}
|
|
|
|
|
|
- (float)drawsRoundCornerBesideArrow {
|
|
return drawsRoundCornerBesideArrow;
|
|
}
|
|
|
|
|
|
- (void)setDrawsRoundCornerBesideArrow:(float)value {
|
|
if (drawsRoundCornerBesideArrow != value) {
|
|
drawsRoundCornerBesideArrow = value;
|
|
|
|
[self _redisplay];
|
|
}
|
|
}
|
|
|
|
|
|
- (void)setBackgroundImage:(NSImage *)value
|
|
{
|
|
if (value) {
|
|
[self setBackgroundColor:[NSColor colorWithPatternImage:value]];
|
|
}
|
|
}
|
|
|
|
|
|
@end
|