From 185a0e78897768420507a6f3d6421285181d5b90 Mon Sep 17 00:00:00 2001 From: preble Date: Sat, 9 Oct 2010 12:15:28 -0400 Subject: [PATCH] Moved item list into main window. Drag & drop works. --- English.lproj/MainMenu.xib | 396 ++++++++---- IJInventoryItem.h | 2 + IJInventoryItem.m | 31 +- IJInventoryView.h | 2 + IJInventoryWindowController.h | 5 + IJInventoryWindowController.m | 79 +++ InsideJob.xcodeproj/project.pbxproj | 14 + MAAttachedWindow.h | 184 ++++++ MAAttachedWindow.m | 951 ++++++++++++++++++++++++++++ 9 files changed, 1519 insertions(+), 145 deletions(-) create mode 100644 MAAttachedWindow.h create mode 100644 MAAttachedWindow.m diff --git a/English.lproj/MainMenu.xib b/English.lproj/MainMenu.xib index 6d77a15..01b9f8a 100644 --- a/English.lproj/MainMenu.xib +++ b/English.lproj/MainMenu.xib @@ -490,9 +490,9 @@ _NSMainMenu - 15 + 7 2 - {{431, 250}, {728, 475}} + {{431, 251}, {744, 474}} 1954021376 Inside Job NSWindow @@ -506,7 +506,7 @@ 266 - {{211, 448}, {509, 14}} + {{211, 447}, {525, 14}} YES @@ -542,7 +542,7 @@ 268 - {{53, 409}, {85, 19}} + {{53, 408}, {85, 19}} YES @@ -585,7 +585,7 @@ NaN - + YES YES @@ -595,7 +595,7 @@ - + 0 0 YES @@ -603,7 +603,7 @@ 1 AAAAAAAAAAAAAAAAAAAAAA - + 3 YES @@ -638,7 +638,7 @@ 268 - {{8, 411}, {40, 14}} + {{8, 410}, {40, 14}} YES @@ -654,7 +654,7 @@ 10 - {{0, 434}, {728, 5}} + {{0, 433}, {744, 5}} {0, 0} @@ -680,7 +680,7 @@ 268 - {{11, 443}, {195, 25}} + {{11, 442}, {195, 25}} YES @@ -724,39 +724,39 @@ 2 - + 268 - {{78, 247}, {432, 144}} + {{78, 246}, {432, 144}} IJInventoryView - + 268 - {{78, 180}, {432, 48}} + {{78, 179}, {432, 48}} IJInventoryView - + 268 - {{11, 199}, {48, 192}} + {{11, 198}, {48, 192}} IJInventoryView - + 265 - {{565, 406}, {152, 22}} + {{556, 405}, {177, 22}} YES - + 343014976 268436544 - + YES 1 @@ -765,9 +765,9 @@ 130560 0 search - + _searchFieldSearch: - + 138690815 0 @@ -794,9 +794,9 @@ - + _searchFieldCancel: - + 138690815 0 @@ -804,32 +804,34 @@ 75 255 + CAAAAA - + - 273 + 4369 YES - - + + 2304 YES - - - 256 - {173, 397} - + + + 4352 + {205, 396} + YES -2147483392 - {{224, 0}, {16, 17}} + {{191, 0}, {16, 17}} YES - + + itemId 40 40 1000 @@ -838,7 +840,7 @@ 2048 - + 3 MC4zMzMzMzI5ODU2AA @@ -849,12 +851,12 @@ - + 337772096 -2080372736 Text Cell - + YES @@ -886,10 +888,10 @@ NaN - + - - + + 3 YES @@ -902,7 +904,7 @@ NO YES - + 6 System @@ -914,10 +916,43 @@ 3 YES YES - + - - 127 + + image + 32 + 10 + 3.4028234663852886e+38 + + 75628096 + 2048 + + + + 6 + System + headerColor + + + + + + 67239424 + 33554432 + + 0 + 0 + 0 + NO + + 3 + YES + YES + + + + name + 124 40 1000 @@ -925,22 +960,22 @@ 2048 - + - + 337772096 2048 Text Cell - + 3 YES YES - + 3 @@ -956,7 +991,7 @@ 17 - -700448768 + -767557632 4 @@ -966,48 +1001,49 @@ 0 - {{1, 1}, {173, 397}} - - - + {{1, 1}, {205, 396}} + + + 4 - - + + -2147483392 - {{224, 17}, {15, 102}} - - + {{191, 17}, {15, 365}} + + _doScroller: - 0.99668874172185429 + 0.9974811083123426 - - + + -2147483392 - {{1, 119}, {223, 15}} - + {{1, 382}, {190, 15}} + 1 - + _doScroller: - 0.99425287356321834 + 0.99514563106796117 - {{554, -1}, {175, 399}} + {{538, -1}, {207, 398}} - + 562 - - - + + + QSAAAEEgAABBmAAAQZgAAA - {728, 475} + {744, 474} {{0, 0}, {1440, 878}} {1.79769e+308, 1.79769e+308} + MainWindow InsideJobAppDelegate @@ -1290,7 +1326,7 @@ inventoryView - + 642 @@ -1298,7 +1334,7 @@ armorView - + 645 @@ -1306,10 +1342,58 @@ quickView - + 646 + + + itemSearchField + + + + 659 + + + + itemTableView + + + + 660 + + + + dataSource + + + + 661 + + + + delegate + + + + 662 + + + + updateItemSearchFilter: + + + + 663 + + + + initialFirstResponder + + + + 668 + @@ -1681,11 +1765,11 @@ - - - - - + + + + + @@ -1799,7 +1883,7 @@ 641 - + YES @@ -1807,95 +1891,110 @@ 643 - + 644 - + 647 - + YES - + 648 - - + + 649 - + YES - - - + + + 650 - - + + 651 - - + + 652 - + YES - - + + + - + 654 - + YES - + - + 655 - + YES - + - + 656 - - + + 657 - + YES - + - + 658 - - + + + + + 665 + + + YES + + + + + + 667 + + @@ -2043,6 +2142,7 @@ 658.IBNumberFormatterBehaviorMetadataKey 658.IBNumberFormatterLocalizesFormatMetadataKey 658.IBPluginDependency + 667.IBPluginDependency 74.IBPluginDependency 74.ImportedFromIB2 75.IBPluginDependency @@ -2142,9 +2242,9 @@ {{475, 832}, {234, 43}} com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - {{80, 119}, {728, 475}} + {{34, 169}, {744, 474}} com.apple.InterfaceBuilder.CocoaPlugin - {{80, 119}, {728, 475}} + {{34, 169}, {744, 474}} {{33, 99}, {480, 360}} {3.40282e+38, 3.40282e+38} @@ -2226,6 +2326,7 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -2259,7 +2360,7 @@ - 658 + 668 @@ -2287,14 +2388,35 @@ IJInventoryWindowController NSWindowController - worldSelectionChanged: - id + YES + + YES + updateItemSearchFilter: + worldSelectionChanged: + + + YES + id + id + - worldSelectionChanged: - - worldSelectionChanged: - id + YES + + YES + updateItemSearchFilter: + worldSelectionChanged: + + + YES + + updateItemSearchFilter: + id + + + worldSelectionChanged: + id + @@ -2303,7 +2425,8 @@ YES armorView inventoryView - outlineView + itemSearchField + itemTableView quickView statusTextField worldSelectionControl @@ -2312,7 +2435,8 @@ YES IJInventoryView IJInventoryView - NSOutlineView + NSSearchField + NSTableView IJInventoryView NSTextField NSSegmentedControl @@ -2324,7 +2448,8 @@ YES armorView inventoryView - outlineView + itemSearchField + itemTableView quickView statusTextField worldSelectionControl @@ -2340,8 +2465,12 @@ IJInventoryView - outlineView - NSOutlineView + itemSearchField + NSSearchField + + + itemTableView + NSTableView quickView @@ -2558,6 +2687,14 @@ Foundation.framework/Headers/NSFormatter.h + + NSImageCell + NSCell + + IBFrameworkSource + AppKit.framework/Headers/NSImageCell.h + + NSMatrix NSControl @@ -2666,7 +2803,7 @@ NSObject - + IBFrameworkSource AppKit.framework/Headers/NSOutlineView.h @@ -2860,11 +2997,6 @@ QuartzCore.framework/Headers/CIImageProvider.h - - NSOutlineView - NSTableView - - NSResponder diff --git a/IJInventoryItem.h b/IJInventoryItem.h index 878b0b5..835a33a 100644 --- a/IJInventoryItem.h +++ b/IJInventoryItem.h @@ -35,4 +35,6 @@ + (NSDictionary *)itemIdLookup; ++ (NSImage *)imageForItemId:(uint16_t)itemId; + @end diff --git a/IJInventoryItem.m b/IJInventoryItem.m index ccc6f49..781f0fb 100644 --- a/IJInventoryItem.m +++ b/IJInventoryItem.m @@ -50,7 +50,7 @@ return [NSString stringWithFormat:@"%d", self.itemId]; } -- (NSImage *)image ++ (NSImage *)imageForItemId:(uint16_t)itemId { NSSize itemImageSize = NSMakeSize(32, 32); NSPoint atlasOffset; @@ -60,29 +60,29 @@ int index; - if (self.itemId <= 85) + if (itemId <= 85) { - if (self.itemId <= 20) - index = self.itemId - 1; // first item is 1 - else if (self.itemId == 35) - index = self.itemId - (35 - 20); - else if (self.itemId >= 37) - index = self.itemId - (37 - 21); + if (itemId <= 20) + index = itemId - 1; // first item is 1 + else if (itemId == 35) + index = itemId - (35 - 20); + else if (itemId >= 37) + index = itemId - (37 - 21); atlasOffset = NSMakePoint(36, 75); } - else if (self.itemId >= 256 && self.itemId <= 346) + else if (itemId >= 256 && itemId <= 346) { - index = self.itemId - 256; + index = itemId - 256; atlasOffset = NSMakePoint(445, 23+52); } - else if (self.itemId >= 2556 && self.itemId <= 2557) + else if (itemId >= 2556 && itemId <= 2557) { - index = self.itemId - 2556; + index = itemId - 2556; atlasOffset = NSMakePoint(445+pixelsPerColumn, 23+52); } else { - NSLog(@"%s error: unrecognized item id %d", __PRETTY_FUNCTION__, self.itemId); + NSLog(@"%s error: unrecognized item id %d", __PRETTY_FUNCTION__, itemId); return nil; } @@ -113,6 +113,11 @@ return output; } +- (NSImage *)image +{ + return [IJInventoryItem imageForItemId:itemId]; +} + + (NSDictionary *)itemIdLookup { static NSDictionary *lookup = nil; diff --git a/IJInventoryView.h b/IJInventoryView.h index fb2c494..c3a872e 100644 --- a/IJInventoryView.h +++ b/IJInventoryView.h @@ -8,6 +8,8 @@ #import +extern NSString * const IJPasteboardTypeInventoryItem; + @protocol IJInventoryViewDelegate; @class IJInventoryItem; diff --git a/IJInventoryWindowController.h b/IJInventoryWindowController.h index 94442e5..4d32b85 100644 --- a/IJInventoryWindowController.h +++ b/IJInventoryWindowController.h @@ -30,6 +30,8 @@ // Search/Item List NSSearchField *itemSearchField; NSTableView *itemTableView; + NSArray *allItemIds; + NSArray *filteredItemIds; // Document BOOL dirty; @@ -41,9 +43,12 @@ @property (nonatomic, assign) IBOutlet IJInventoryView *inventoryView; @property (nonatomic, assign) IBOutlet IJInventoryView *quickView; @property (nonatomic, assign) IBOutlet IJInventoryView *armorView; +@property (nonatomic, assign) IBOutlet NSSearchField *itemSearchField; +@property (nonatomic, assign) IBOutlet NSTableView *itemTableView; @property (nonatomic, retain) NSNumber *worldTime; - (IBAction)worldSelectionChanged:(id)sender; +- (IBAction)updateItemSearchFilter:(id)sender; @end diff --git a/IJInventoryWindowController.m b/IJInventoryWindowController.m index 18078f4..1e964ec 100644 --- a/IJInventoryWindowController.m +++ b/IJInventoryWindowController.m @@ -17,6 +17,7 @@ @synthesize worldSelectionControl; @synthesize statusTextField; @synthesize inventoryView, armorView, quickView; +@synthesize itemSearchField, itemTableView; - (void)awakeFromNib @@ -32,6 +33,12 @@ inventoryView.delegate = self; quickView.delegate = self; armorView.delegate = self; + + // Item Table View setup + NSArray *keys = [[IJInventoryItem itemIdLookup] allKeys]; + keys = [keys sortedArrayUsingSelector:@selector(compare:)]; + allItemIds = [[NSArray alloc] initWithArray:keys]; + filteredItemIds = [allItemIds retain]; } - (void)dealloc @@ -278,5 +285,77 @@ #pragma mark Item Picker +- (IBAction)updateItemSearchFilter:(id)sender +{ + NSString *filterString = [sender stringValue]; + + if (filterString.length == 0) + { + [filteredItemIds autorelease]; + filteredItemIds = [allItemIds retain]; + [itemTableView reloadData]; + return; + } + + NSMutableArray *results = [NSMutableArray array]; + + for (NSNumber *itemId in allItemIds) + { + NSString *name = [[IJInventoryItem itemIdLookup] objectForKey:itemId]; + NSRange range = [name rangeOfString:filterString options:NSCaseInsensitiveSearch]; + if (range.location != NSNotFound) + [results addObject:itemId]; + } + + [filteredItemIds autorelease]; + filteredItemIds = [results retain]; + [itemTableView reloadData]; +} + +- (NSInteger)numberOfRowsInTableView:(NSTableView *)theTableView +{ + return filteredItemIds.count; +} +- (id)tableView:(NSTableView *)theTableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row +{ + // TODO: Change this, because the row will not correspond once we support sorting. + NSNumber *itemId = [filteredItemIds objectAtIndex:row]; + + if ([tableColumn.identifier isEqual:@"itemId"]) + { + return itemId; + } + else if ([tableColumn.identifier isEqual:@"image"]) + { + return [IJInventoryItem imageForItemId:[itemId shortValue]]; + } + else + { + NSString *name = [[IJInventoryItem itemIdLookup] objectForKey:itemId]; + return name; + } +} +- (BOOL)tableView:(NSTableView *)tableView writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard +{ + [pboard declareTypes:[NSArray arrayWithObjects:IJPasteboardTypeInventoryItem, nil] owner:nil]; + + NSNumber *itemId = [filteredItemIds objectAtIndex:[rowIndexes firstIndex]]; + + IJInventoryItem *item = [[IJInventoryItem alloc] init]; + item.itemId = [itemId shortValue]; + item.count = 1; + item.damage = 0; + item.slot = 0; + + [pboard setData:[NSKeyedArchiver archivedDataWithRootObject:item] + forType:IJPasteboardTypeInventoryItem]; + + [item release]; + + return YES; +} + + + @end diff --git a/InsideJob.xcodeproj/project.pbxproj b/InsideJob.xcodeproj/project.pbxproj index 718031e..b9ea616 100644 --- a/InsideJob.xcodeproj/project.pbxproj +++ b/InsideJob.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 668B297C125E5DF00060BF71 /* ItemPicker.xib in Resources */ = {isa = PBXBuildFile; fileRef = 668B297B125E5DF00060BF71 /* ItemPicker.xib */; }; 66BC00031260215C005A23F4 /* IJInventoryView.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BC00021260215C005A23F4 /* IJInventoryView.m */; }; 66BC000E12602359005A23F4 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66BC000D12602359005A23F4 /* QuartzCore.framework */; }; + 66BC033B1260CC59005A23F4 /* MAAttachedWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 66BC033A1260CC59005A23F4 /* MAAttachedWindow.m */; }; 66BCFC2B125E9A51005A23F4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 66BCFC2A125E9A51005A23F4 /* Credits.rtf */; }; 66BCFC36125EA53E005A23F4 /* InsideJob.icns in Resources */ = {isa = PBXBuildFile; fileRef = 66BCFC35125EA53E005A23F4 /* InsideJob.icns */; }; 66BCFE62125FCEC6005A23F4 /* DataValuesV110Transparent.png in Resources */ = {isa = PBXBuildFile; fileRef = 66BCFE61125FCEC6005A23F4 /* DataValuesV110Transparent.png */; }; @@ -57,6 +58,8 @@ 66BC00011260215C005A23F4 /* IJInventoryView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IJInventoryView.h; sourceTree = ""; }; 66BC00021260215C005A23F4 /* IJInventoryView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IJInventoryView.m; sourceTree = ""; }; 66BC000D12602359005A23F4 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; + 66BC03391260CC59005A23F4 /* MAAttachedWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MAAttachedWindow.h; sourceTree = ""; }; + 66BC033A1260CC59005A23F4 /* MAAttachedWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MAAttachedWindow.m; sourceTree = ""; }; 66BCFC2A125E9A51005A23F4 /* Credits.rtf */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; 66BCFC35125EA53E005A23F4 /* InsideJob.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = InsideJob.icns; sourceTree = ""; }; 66BCFE61125FCEC6005A23F4 /* DataValuesV110Transparent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = DataValuesV110Transparent.png; sourceTree = ""; }; @@ -85,6 +88,7 @@ 256AC3D90F4B6AC300CF3369 /* InsideJobAppDelegate.m */, 668B27F3125D96470060BF71 /* Interface */, 668B2551125D59BF0060BF71 /* Model */, + 66BC033E1260CC68005A23F4 /* Views & Windows */, 668B2559125D5BB90060BF71 /* Categories */, ); name = Classes; @@ -198,6 +202,15 @@ name = Interface; sourceTree = ""; }; + 66BC033E1260CC68005A23F4 /* Views & Windows */ = { + isa = PBXGroup; + children = ( + 66BC03391260CC59005A23F4 /* MAAttachedWindow.h */, + 66BC033A1260CC59005A23F4 /* MAAttachedWindow.m */, + ); + name = "Views & Windows"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -277,6 +290,7 @@ 668B27F2125D963F0060BF71 /* IJInventoryWindowController.m in Sources */, 668B2979125E5DD40060BF71 /* IJItemPickerWindowController.m in Sources */, 66BC00031260215C005A23F4 /* IJInventoryView.m in Sources */, + 66BC033B1260CC59005A23F4 /* MAAttachedWindow.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MAAttachedWindow.h b/MAAttachedWindow.h new file mode 100644 index 0000000..731452b --- /dev/null +++ b/MAAttachedWindow.h @@ -0,0 +1,184 @@ +// +// MAAttachedWindow.h +// +// Created by Matt Gemmell on 27/09/2007. +// Copyright 2007 Magic Aubergine. +// + +#import + +/* + Below are the positions the attached window can be displayed at. + + Note that these positions are relative to the point passed to the constructor, + e.g. MAPositionBottomRight will put the window below the point and towards the right, + MAPositionTop will horizontally center the window above the point, + MAPositionRightTop will put the window to the right and above the point, + and so on. + + You can also pass MAPositionAutomatic (or use an initializer which omits the 'onSide:' + argument) and the attached window will try to position itself sensibly, based on + available screen-space. + + Notes regarding automatically-positioned attached windows: + + (a) The window prefers to position itself horizontally centered below the specified point. + This gives a certain enhanced visual sense of an attachment/relationship. + + (b) The window will try to align itself with its parent window (if any); i.e. it will + attempt to stay within its parent window's frame if it can. + + (c) The algorithm isn't perfect. :) If in doubt, do your own calculations and then + explicitly request that the window attach itself to a particular side. + */ + +typedef enum _MAWindowPosition { + // The four primary sides are compatible with the preferredEdge of NSDrawer. + MAPositionLeft = NSMinXEdge, // 0 + MAPositionRight = NSMaxXEdge, // 2 + MAPositionTop = NSMaxYEdge, // 3 + MAPositionBottom = NSMinYEdge, // 1 + MAPositionLeftTop = 4, + MAPositionLeftBottom = 5, + MAPositionRightTop = 6, + MAPositionRightBottom = 7, + MAPositionTopLeft = 8, + MAPositionTopRight = 9, + MAPositionBottomLeft = 10, + MAPositionBottomRight = 11, + MAPositionAutomatic = 12 +} MAWindowPosition; + +@interface MAAttachedWindow : NSWindow { + NSColor *borderColor; + float borderWidth; + float viewMargin; + float arrowBaseWidth; + float arrowHeight; + BOOL hasArrow; + float cornerRadius; + BOOL drawsRoundCornerBesideArrow; + + @private + NSColor *_MABackgroundColor; + __weak NSView *_view; + __weak NSWindow *_window; + NSPoint _point; + MAWindowPosition _side; + float _distance; + NSRect _viewFrame; + BOOL _resizing; +} + +/* + Initialization methods + + Parameters: + + view The view to display in the attached window. Must not be nil. + + point The point to which the attached window should be attached. If you + are also specifying a parent window, the point should be in the + coordinate system of that parent window. If you are not specifying + a window, the point should be in the screen's coordinate space. + This value is required. + + window The parent window to attach this one to. Note that no actual + relationship is created (particularly, this window is not made + a childWindow of the parent window). + Default: nil. + + side The side of the specified point on which to attach this window. + Default: MAPositionAutomatic. + + distance How far from the specified point this window should be. + Default: 0. + */ + +- (MAAttachedWindow *)initWithView:(NSView *)view // designated initializer + attachedToPoint:(NSPoint)point + inWindow:(NSWindow *)window + onSide:(MAWindowPosition)side + atDistance:(float)distance; +- (MAAttachedWindow *)initWithView:(NSView *)view + attachedToPoint:(NSPoint)point + inWindow:(NSWindow *)window + atDistance:(float)distance; +- (MAAttachedWindow *)initWithView:(NSView *)view + attachedToPoint:(NSPoint)point + onSide:(MAWindowPosition)side + atDistance:(float)distance; +- (MAAttachedWindow *)initWithView:(NSView *)view + attachedToPoint:(NSPoint)point + atDistance:(float)distance; +- (MAAttachedWindow *)initWithView:(NSView *)view + attachedToPoint:(NSPoint)point + inWindow:(NSWindow *)window; +- (MAAttachedWindow *)initWithView:(NSView *)view + attachedToPoint:(NSPoint)point + onSide:(MAWindowPosition)side; +- (MAAttachedWindow *)initWithView:(NSView *)view + attachedToPoint:(NSPoint)point; + +// Accessor methods +- (void)setPoint:(NSPoint)point side:(MAWindowPosition)side; +- (NSColor *)borderColor; +- (void)setBorderColor:(NSColor *)value; +- (float)borderWidth; +- (void)setBorderWidth:(float)value; // See note 1 below. +- (float)viewMargin; +- (void)setViewMargin:(float)value; // See note 2 below. +- (float)arrowBaseWidth; +- (void)setArrowBaseWidth:(float)value; // See note 2 below. +- (float)arrowHeight; +- (void)setArrowHeight:(float)value; // See note 2 below. +- (float)hasArrow; +- (void)setHasArrow:(float)value; +- (float)cornerRadius; +- (void)setCornerRadius:(float)value; // See note 2 below. +- (float)drawsRoundCornerBesideArrow; // See note 3 below. +- (void)setDrawsRoundCornerBesideArrow:(float)value; // See note 2 below. +- (void)setBackgroundImage:(NSImage *)value; +- (NSColor *)windowBackgroundColor; // See note 4 below. +- (void)setBackgroundColor:(NSColor *)value; + +/* + Notes regarding accessor methods: + + 1. The border is drawn inside the viewMargin area, expanding inwards; it does not + increase the width/height of the window. You can use the -setBorderWidth: and + -setViewMargin: methods together to achieve the exact look/geometry you want. + (viewMargin is the distance between the edge of the view and the window edge.) + + 2. The specified setter methods are primarily intended to be used _before_ the window + is first shown. If you use them while the window is already visible, be aware + that they may cause the window to move and/or resize, in order to stay anchored + to the point specified in the initializer. They may also cause the view to move + within the window, in order to remain centered there. + + Note that the -setHasArrow: method can safely be used at any time, and will not + cause moving/resizing of the window. This is for convenience, in case you want + to add or remove the arrow in response to user interaction. For example, you + could make the attached window movable by its background, and if the user dragged + it away from its initial point, the arrow could be removed. This would duplicate + how Aperture's attached windows behave. + + 3. drawsRoundCornerBesideArrow takes effect when the arrow is being drawn at a corner, + i.e. when it's not at one of the four primary compass directions. In this situation, + if drawsRoundCornerBesideArrow is YES (the default), then that corner of the window + will be rounded just like the other three corners, thus the arrow will be inset + slightly from the edge of the window to allow room for the rounded corner. If this + value is NO, the corner beside the arrow will be a square corner, and the other + three corners will be rounded. + + This is useful when you want to attach a window very near the edge of another window, + and don't want the attached window's edge to be visually outside the frame of the + parent window. + + 4. Note that to retrieve the background color of the window, you should use the + -windowBackgroundColor method, instead of -backgroundColor. This is because we draw + the entire background of the window (rounded path, arrow, etc) in an NSColor pattern + image, and set it as the backgroundColor of the window. + */ + +@end diff --git a/MAAttachedWindow.m b/MAAttachedWindow.m new file mode 100644 index 0000000..ea2fc46 --- /dev/null +++ b/MAAttachedWindow.m @@ -0,0 +1,951 @@ +// +// 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