2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIListWindowController.h"
19 #import "AISCLViewPlugin.h"
20 #import <Adium/AIListOutlineView.h>
21 #import <Adium/AIChatControllerProtocol.h>
22 #import <Adium/AIAccountControllerProtocol.h>
23 #import <Adium/AIInterfaceControllerProtocol.h>
24 #import <Adium/AIPreferenceControllerProtocol.h>
25 #import <Adium/AIDockControllerProtocol.h>
26 #import <AIUtilities/AIWindowAdditions.h>
27 #import <AIUtilities/AIFunctions.h>
28 #import <AIUtilities/AIWindowControllerAdditions.h>
29 #import <AIUtilities/AIApplicationAdditions.h>
30 #import <AIListBookmark.h>
31 #import <Adium/AIListContact.h>
32 #import <Adium/AIListGroup.h>
33 #import <Adium/AIListObject.h>
34 #import <Adium/AIUserIcons.h>
35 #import <AIUtilities/AIDockingWindow.h>
36 #import <AIUtilities/AIEventAdditions.h>
38 #import <AIUtilities/AITigerCompatibility.h>
40 #define PREF_GROUP_CONTACT_LIST @"Contact List"
42 #define SLIDE_ALLOWED_RECT_EDGE_MASK (AIMinXEdgeMask | AIMaxXEdgeMask)
43 #define DOCK_HIDING_MOUSE_POLL_INTERVAL 0.1
44 #define WINDOW_ALIGNMENT_TOLERANCE 2.0f
45 #define MOUSE_EDGE_SLIDE_ON_DISTANCE 1.1f
46 #define WINDOW_SLIDING_MOUSE_DISTANCE_TOLERANCE 3.0f
48 #define SNAP_DISTANCE 15.0
50 @interface AIListWindowController (PRIVATE)
51 - (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList;
52 + (NSString *)nibName;
54 - (void)_configureAutoResizing;
55 - (void)_configureToolbar;
56 + (void)updateScreenSlideBoundaryRect:(id)sender;
57 - (BOOL)shouldSlideWindowOffScreen_mousePositionStrategy;
58 - (void)slideWindowIfNeeded:(id)sender;
59 - (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy;
60 - (BOOL)shouldSlideWindowOnScreen_adiumActiveStrategy;
61 - (BOOL)shouldSlideWindowOffScreen_adiumActiveStrategy;
62 - (void)setSavedFrame:(NSRect)f;
63 - (void)restoreSavedFrame;
65 - (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime;
68 @implementation AIListWindowController
72 if ([self isEqual:[AIListWindowController class]]) {
73 [[NSNotificationCenter defaultCenter] addObserver:self
74 selector:@selector(updateScreenSlideBoundaryRect:)
75 name:NSApplicationDidChangeScreenParametersNotification
78 [self updateScreenSlideBoundaryRect:nil];
82 //Return a new contact list window controller
83 + (AIListWindowController *)listWindowController
85 return [[[self alloc] initWithContactList:nil] autorelease];
88 + (AIListWindowController *)listWindowControllerForContactList:(AIListObject<AIContainingObject> *)contactList
90 return [[[self alloc] initWithContactList:contactList] autorelease];
93 - (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList
95 if ((self = [self initWithWindowNibName:[[self class] nibName]])) {
98 [self setContactList:contactList];
104 - (AIListObject<AIContainingObject> *)contactList
106 return (contactListRoot ? contactListRoot : [contactListController contactList]);
109 - (AIListController *) listController
111 return contactListController;
114 - (AIListOutlineView *)contactListView
116 return contactListView;
119 - (void)setContactList:(AIListObject<AIContainingObject> *)inContactList
121 if (inContactList != contactListRoot) {
122 [contactListRoot release];
123 contactListRoot = [inContactList retain];
127 //Our window nib name
128 + (NSString *)nibName
133 - (Class)listControllerClass
135 return [AIListController class];
140 [[NSNotificationCenter defaultCenter] removeObserver:self];
142 [contactListController close];
143 [windowLastScreen release];
149 - (NSString *)adiumFrameAutosaveName
151 AILogWithSignature(@"My autosave name is %@",[NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]]);
152 return [NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]];
155 //Setup the window after it has loaded
156 - (void)windowDidLoad
158 contactListController = [[[self listControllerClass] alloc] initWithContactList:[self contactList]
159 inOutlineView:contactListView
160 inScrollView:scrollView_contactList
163 //super's windowDidLoad will restore our location, which is based upon the contactListRoot
164 [super windowDidLoad];
166 //Exclude this window from the window menu (since we add it manually)
167 [[self window] setExcludedFromWindowsMenu:YES];
168 [[self window] useOptimizedDrawing:YES];
170 minWindowSize = [[self window] minSize];
171 [contactListController setMinWindowSize:minWindowSize];
173 [[self window] setTitle:AILocalizedString(@"Contacts","Contact List window title")];
175 //Watch for resolution and screen configuration changes
176 [[NSNotificationCenter defaultCenter] addObserver:self
177 selector:@selector(screenParametersChanged:)
178 name:NSApplicationDidChangeScreenParametersNotification
181 //Show the contact list initially even if it is at a screen edge and supposed to slide out of view
182 [self delayWindowSlidingForInterval:5];
184 id<AIPreferenceController> preferenceController = [adium preferenceController];
185 //Observe preference changes
186 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST];
187 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
189 //Preference code below assumes layout is done before theme.
190 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_LAYOUT];
191 [preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
193 [[NSNotificationCenter defaultCenter] addObserver:self
194 selector:@selector(applicationDidUnhide:)
195 name:NSApplicationDidUnhideNotification
198 //Save our frame immediately for sliding purposes
199 [self setSavedFrame:[[self window] frame]];
202 //Close the contact list window
203 - (void)windowWillClose:(NSNotification *)notification
205 if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
206 //Hide the window while it's still off-screen
207 [[self window] setAlphaValue:0.0];
209 //Then move it back on screen so that we'll save the proper position in -[AIWindowController windowWillClose:]
210 [self slideWindowOnScreenWithAnimation:NO];
213 [super windowWillClose:notification];
215 //Invalidate the dock-like hiding timer
216 [slideWindowIfNeededTimer invalidate]; [slideWindowIfNeededTimer release];
219 [[adium preferenceController] unregisterPreferenceObserver:self];
220 [[NSNotificationCenter defaultCenter] removeObserver:self];
221 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
223 //Tell the interface to unload our window
224 NSNotificationCenter *adiumNotificationCenter = [adium notificationCenter];
225 [adiumNotificationCenter postNotificationName:Interface_ContactListDidResignMain object:self];
226 [adiumNotificationCenter postNotificationName:Interface_ContactListDidClose object:self];
229 int levelForAIWindowLevel(AIWindowLevel windowLevel)
233 switch (windowLevel) {
234 case AINormalWindowLevel: level = NSNormalWindowLevel; break;
235 case AIFloatingWindowLevel: level = NSFloatingWindowLevel; break;
236 case AIDesktopWindowLevel: level = kCGBackstopMenuLevel; break;
237 default: level = NSNormalWindowLevel; break;
243 - (void)setWindowLevel:(int)level
245 [[self window] setLevel:level];
246 if (![NSApp isOnLeopardOrBetter]) {
247 [[self window] setIgnoresExpose:(level == kCGBackstopMenuLevel)]; //Ignore expose while on the desktop
251 //Preferences have changed
252 - (void)preferencesChangedForGroup:(NSString *)group
254 object:(AIListObject *)object
255 preferenceDict:(NSDictionary *)prefDict
256 firstTime:(BOOL)firstTime
258 BOOL shouldRevealWindowAndDelaySliding = NO;
260 if ([group isEqualToString:PREF_GROUP_CONTACT_LIST]) {
261 AIWindowLevel windowLevel = [[prefDict objectForKey:KEY_CL_WINDOW_LEVEL] intValue];
263 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
265 listHasShadow = [[prefDict objectForKey:KEY_CL_WINDOW_HAS_SHADOW] boolValue];
266 [[self window] setHasShadow:listHasShadow];
268 windowHidingStyle = [[prefDict objectForKey:KEY_CL_WINDOW_HIDING_STYLE] intValue];
269 slideOnlyInBackground = [[prefDict objectForKey:KEY_CL_SLIDE_ONLY_IN_BACKGROUND] boolValue];
271 [[self window] setHidesOnDeactivate:(windowHidingStyle == AIContactListWindowHidingStyleBackground)];
273 if ([[NSApplication sharedApplication] isOnLeopardOrBetter]) {
274 if (windowHidingStyle == AIContactListWindowHidingStyleSliding || [[prefDict objectForKey:KEY_CL_ALL_SPACES] boolValue])
275 [[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
277 [[self window] setCollectionBehavior:NSWindowCollectionBehaviorDefault];
280 if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
281 if (!slideWindowIfNeededTimer) {
282 slideWindowIfNeededTimer = [[NSTimer scheduledTimerWithTimeInterval:DOCK_HIDING_MOUSE_POLL_INTERVAL
284 selector:@selector(slideWindowIfNeeded:)
286 repeats:YES] retain];
289 } else if (slideWindowIfNeededTimer) {
290 [slideWindowIfNeededTimer invalidate];
291 [slideWindowIfNeededTimer release]; slideWindowIfNeededTimer = nil;
294 [contactListController setShowTooltips:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS] boolValue]];
295 [contactListController setShowTooltipsInBackground:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS_IN_BACKGROUND] boolValue]];
299 if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
300 int windowStyle = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] intValue];
301 BOOL autoResizeVertically = [[prefDict objectForKey:KEY_LIST_LAYOUT_VERTICAL_AUTOSIZE] boolValue];
302 BOOL autoResizeHorizontally = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_AUTOSIZE] boolValue];
303 int forcedWindowWidth, maxWindowWidth;
305 if (autoResizeHorizontally) {
306 //If autosizing, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH determines the maximum width; no forced width.
307 maxWindowWidth = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_WIDTH] intValue];
308 forcedWindowWidth = -1;
310 if (windowStyle == AIContactListWindowStyleStandard/* || windowStyle == AIContactListWindowStyleBorderless*/) {
311 //In the non-transparent non-autosizing modes, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH has no meaning
312 maxWindowWidth = 10000;
313 forcedWindowWidth = -1;
315 //In the transparent non-autosizing modes, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH determines the width of the window
316 forcedWindowWidth = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_WIDTH] intValue];
317 maxWindowWidth = forcedWindowWidth;
321 //Show the resize indicator if either or both of the autoresizing options is NO
322 [[self window] setShowsResizeIndicator:!(autoResizeVertically && autoResizeHorizontally)];
325 Reset the minimum and maximum sizes in case [self contactListDesiredSizeChanged]; doesn't cause a sizing change
326 (and therefore the min and max sizes aren't set there).
328 NSSize thisMinimumSize = minWindowSize;
329 NSSize thisMaximumSize = NSMakeSize(maxWindowWidth, 10000);
330 NSRect currentFrame = [[self window] frame];
332 if (forcedWindowWidth != -1) {
334 If we have a forced width but we are doing no autoresizing, set our frame now so we don't have to be doing checks every time
335 contactListDesiredSizeChanged is called.
337 if (!(autoResizeVertically || autoResizeHorizontally)) {
338 thisMinimumSize.width = forcedWindowWidth;
339 [[self window] setFrame:NSMakeRect(currentFrame.origin.x,currentFrame.origin.y,forcedWindowWidth,currentFrame.size.height)
345 //If vertically resizing, make the minimum and maximum heights the current height
346 if (autoResizeVertically) {
347 thisMinimumSize.height = currentFrame.size.height;
348 thisMaximumSize.height = currentFrame.size.height;
351 //If horizontally resizing, make the minimum and maximum widths the current width
352 if (autoResizeHorizontally) {
353 thisMinimumSize.width = currentFrame.size.width;
354 thisMaximumSize.width = currentFrame.size.width;
357 /* For a standard window, inform the contact list that, if asked, it wants to be 175 pixels or more.
358 * A maximum width less than this can make the list autosize smaller, but if it has its druthers it'll be a sane
361 [contactListView setMinimumDesiredWidth:((windowStyle == AIContactListWindowStyleStandard) ? 175 : 0)];
363 [[self window] setMinSize:thisMinimumSize];
364 [[self window] setMaxSize:thisMaximumSize];
366 [contactListController setAutoresizeHorizontally:autoResizeHorizontally];
367 [contactListController setAutoresizeVertically:autoResizeVertically];
368 [contactListController setForcedWindowWidth:forcedWindowWidth];
369 [contactListController setMaxWindowWidth:maxWindowWidth];
371 [contactListController contactListDesiredSizeChanged];
374 shouldRevealWindowAndDelaySliding = YES;
379 if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
380 float opacity = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_OPACITY] floatValue];
381 [contactListController setBackgroundOpacity:opacity];
384 shouldRevealWindowAndDelaySliding = YES;
388 //Layout and Theme ------------
389 BOOL groupLayout = ([group isEqualToString:PREF_GROUP_LIST_LAYOUT]);
390 BOOL groupTheme = ([group isEqualToString:PREF_GROUP_LIST_THEME]);
391 if (groupLayout || (groupTheme && !firstTime)) { /* We don't want to execute this code twice when initializing */
392 NSDictionary *layoutDict = [[adium preferenceController] preferencesForGroup:PREF_GROUP_LIST_LAYOUT];
393 NSDictionary *themeDict = [[adium preferenceController] preferencesForGroup:PREF_GROUP_LIST_THEME];
397 int iconSize = [[layoutDict objectForKey:KEY_LIST_LAYOUT_USER_ICON_SIZE] intValue];
398 [AIUserIcons setListUserIconSize:NSMakeSize(iconSize,iconSize)];
402 if (groupTheme || firstTime) {
403 NSString *imagePath = [themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_PATH];
406 if (imagePath && [imagePath length] && [[themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_ENABLED] boolValue]) {
407 [contactListView setBackgroundImage:[[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease]];
409 [contactListView setBackgroundImage:nil];
413 //Both layout and theme
414 [contactListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
417 shouldRevealWindowAndDelaySliding = YES;
421 if (shouldRevealWindowAndDelaySliding) {
422 [self delayWindowSlidingForInterval:2];
423 [self slideWindowOnScreenWithAnimation:NO];
426 //Do a slide immediately if needed (to display as per our new preferneces)
427 [self slideWindowIfNeeded:nil];
432 - (IBAction)performDefaultActionOnSelectedObject:(AIListObject *)selectedObject sender:(NSOutlineView *)sender
434 if ([selectedObject isKindOfClass:[AIListGroup class]]) {
435 //Expand or collapse the group
436 if ([sender isItemExpanded:selectedObject]) {
437 [sender collapseItem:selectedObject];
439 [sender expandItem:selectedObject];
442 } else if ([selectedObject isMemberOfClass:[AIListBookmark class]]) {
443 //Hide any tooltip the contactListController is currently showing
444 [contactListController hideTooltip];
446 [(AIListBookmark *)selectedObject openChat];
448 } else if ([selectedObject isKindOfClass:[AIListContact class]]) {
449 //Hide any tooltip the contactListController is currently showing
450 [contactListController hideTooltip];
452 //Open a new message with the contact
453 [[adium interfaceController] setActiveChat:[[adium chatController] openChatWithContact:(AIListContact *)selectedObject
454 onPreferredAccount:YES]];
460 - (BOOL) canCustomizeToolbar
465 //Interface Container --------------------------------------------------------------------------------------------------
466 #pragma mark Interface Container
467 //Close this container
468 - (void)close:(id)sender
470 //In response to windowShouldClose, the interface controller releases us. At that point, no one would be retaining
471 //this instance of AIContactListWindowController, and we would be deallocated. The call to [self window] will
472 //crash if we are deallocated. A dirty, but functional fix is to temporarily retain ourself here.
475 if ([self windowShouldClose:nil]) {
476 [[self window] close];
482 - (void)makeActive:(id)sender
484 [[self window] makeKeyAndOrderFront:self];
488 //Contact list brought to front
489 - (void)windowDidBecomeKey:(NSNotification *)notification
491 [[adium notificationCenter] postNotificationName:Interface_ContactListDidBecomeMain object:self];
494 //Contact list sent back
495 - (void)windowDidResignKey:(NSNotification *)notification
497 [[adium notificationCenter] postNotificationName:Interface_ContactListDidResignMain object:self];
501 - (void)showWindowInFrontIfAllowed:(BOOL)inFront
503 //Always show for three seconds at least if we're told to show
504 [self delayWindowSlidingForInterval:3];
506 //Call super to actually do the showing
507 [super showWindowInFrontIfAllowed:inFront];
509 NSWindow *window = [self window];
511 if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
512 [self slideWindowOnScreenWithAnimation:NO];
515 windowSlidOffScreenEdgeMask = AINoEdges;
517 currentScreen = [window screen];
518 currentScreenFrame = [currentScreen frame];
520 if ([[NSScreen screens] count] &&
521 (currentScreen == [[NSScreen screens] objectAtIndex:0])) {
522 currentScreenFrame.size.height -= [NSMenuView menuBarHeight];
525 //Ensure the window is displaying at the proper level and exposé setting
526 AIWindowLevel windowLevel = [[[adium preferenceController] preferenceForKey:KEY_CL_WINDOW_LEVEL
527 group:PREF_GROUP_CONTACT_LIST] intValue];
528 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
531 - (void)setSavedFrame:(NSRect)frame
534 [[self window] saveFrameUsingName:@"SavedContactListFrame"];
537 - (void)restoreSavedFrame
539 NSWindow *myWindow = [self window];
540 [myWindow setFrameUsingName:@"SavedContactListFrame" force:YES];
541 oldFrame = [myWindow frame];
544 // Auto-resizing support ------------------------------------------------------------------------------------------------
545 #pragma mark Auto-resizing support
547 - (void)screenParametersChanged:(NSNotification *)notification
549 NSWindow *window = [self window];
551 NSScreen *windowScreen = [window screen];
553 if ([[NSScreen screens] containsObject:windowLastScreen]) {
554 windowScreen = windowLastScreen;
556 [windowLastScreen release]; windowLastScreen = nil;
557 windowScreen = [NSScreen mainScreen];
561 NSRect newScreenFrame = [windowScreen frame];
563 if ([[NSScreen screens] count] &&
564 (windowScreen == [[NSScreen screens] objectAtIndex:0])) {
565 newScreenFrame.size.height -= [NSMenuView menuBarHeight];
568 NSRect listFrame = [window frame];
570 oldFrame.origin.x *= ((newScreenFrame.size.width - listFrame.size.width) / ((currentScreenFrame.size.width - listFrame.size.width) + 0.00001));
571 oldFrame.origin.y *= ((newScreenFrame.size.height - listFrame.size.height) / ((currentScreenFrame.size.height - listFrame.size.height) + 0.00001));
573 [self delayWindowSlidingForInterval:2];
574 [self slideWindowOnScreenWithAnimation:NO];
576 [contactListController contactListDesiredSizeChanged];
578 currentScreen = [window screen];
579 currentScreenFrame = newScreenFrame;
580 [self setSavedFrame:[window frame]];
584 #pragma mark Printing
585 - (void)adiumPrint:(id)sender
587 [contactListView print:sender];
590 // Dock-like hiding -----------------------------------------------------------------------------------------------------
591 #pragma mark Dock-like hiding
593 /* screenSlideBoundaryRect is the rect that the contact list slides in and out of for dock-like hiding
594 * screenSlideBoundaryRect = (menubarScreen frame without menubar) union (union of frames of all other screens)
596 static NSRect screenSlideBoundaryRect = { {0.0f, 0.0f}, {0.0f, 0.0f} };
597 + (void)updateScreenSlideBoundaryRect:(id)sender
599 NSArray *screens = [NSScreen screens];
600 int numScreens = [screens count];
602 if (numScreens > 0) {
603 //The menubar screen is a special case - the menubar is not a part of the rect we're interested in
604 NSScreen *menubarScreen = [screens objectAtIndex:0];
605 screenSlideBoundaryRect = [menubarScreen frame];
606 screenSlideBoundaryRect.size.height = NSMaxY([menubarScreen visibleFrame]) - NSMinY([menubarScreen frame]);
607 for (int i = 1; i < numScreens; i++) {
608 screenSlideBoundaryRect = NSUnionRect(screenSlideBoundaryRect, [[screens objectAtIndex:i] frame]);
616 * If the contact list is open but not visible when we unhide, we should always display it; it should not, however, steal focus.
618 - (void)applicationDidUnhide:(NSNotification *)notification
620 if (![[self window] isVisible]) {
621 [self showWindowInFrontIfAllowed:NO];
625 - (BOOL)windowShouldHideOnDeactivate
627 return (windowHidingStyle == AIContactListWindowHidingStyleBackground);
630 - (void)slideWindowIfNeeded:(id)sender
632 if ([self shouldSlideWindowOnScreen]) {
633 //If we're hiding the window (generally) but now sliding it on screen, make sure it's on top
634 if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
635 [self setWindowLevel:NSFloatingWindowLevel];
636 overrodeWindowLevel = YES;
639 [self slideWindowOnScreen];
641 } else if ([self shouldSlideWindowOffScreen]) {
642 AIRectEdgeMask adjacentEdges = [self slidableEdgesAdjacentToWindow];
644 if (adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask)) {
645 [self slideWindowOffScreenEdges:(adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask))];
647 [self slideWindowOffScreenEdges:adjacentEdges];
650 /* If we're hiding the window (generally) but now sliding it off screen, set it to kCGBackstopMenuLevel and don't
651 * let it participate in expose.
653 if (overrodeWindowLevel &&
654 windowHidingStyle == AIContactListWindowHidingStyleSliding) {
655 [self setWindowLevel:kCGBackstopMenuLevel];
656 overrodeWindowLevel = YES;
659 } else if (overrodeWindowLevel &&
660 ([self slidableEdgesAdjacentToWindow] == AINoEdges) &&
661 ([self windowSlidOffScreenEdgeMask] == AINoEdges)) {
662 /* If the window level was overridden at some point and now we:
663 * 1. Are on screen AND
664 * 2. No longer have any edges eligible for sliding
665 * we should restore our window level.
667 AIWindowLevel windowLevel = [[[adium preferenceController] preferenceForKey:KEY_CL_WINDOW_LEVEL
668 group:PREF_GROUP_CONTACT_LIST] intValue];
669 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
670 overrodeWindowLevel = NO;
674 - (BOOL)shouldSlideWindowOnScreen
676 BOOL shouldSlide = NO;
678 if (([self windowSlidOffScreenEdgeMask] != AINoEdges) &&
680 if (slideOnlyInBackground && [NSApp isActive]) {
681 //We only slide while in the background, and the app is not in the background. Slide on screen.
684 } else if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
685 //Slide on screen if the mouse position indicates we should
686 shouldSlide = [self shouldSlideWindowOnScreen_mousePositionStrategy];
688 //It's slid off-screen... and it's not supposed to be sliding at all. Slide back on screen!
696 - (BOOL)shouldSlideWindowOffScreen
698 BOOL shouldSlide = NO;
700 if ((windowHidingStyle == AIContactListWindowHidingStyleSliding) &&
702 ([self windowSlidOffScreenEdgeMask] == AINoEdges) &&
703 (!(slideOnlyInBackground && [NSApp isActive]))) {
704 shouldSlide = [self shouldSlideWindowOffScreen_mousePositionStrategy];
710 // slide off screen if the window is aligned to a screen edge and the mouse is not in the strip of screen
711 // you'd get by translating the window along the screen edge. This is the dock's behavior.
712 - (BOOL)shouldSlideWindowOffScreen_mousePositionStrategy
714 BOOL shouldSlideOffScreen = NO;
716 NSWindow *window = [self window];
717 NSRect windowFrame = [window frame];
718 NSPoint mouseLocation = [NSEvent mouseLocation];
720 AIRectEdgeMask slidableEdgesAdjacentToWindow = [self slidableEdgesAdjacentToWindow];
721 NSRectEdge screenEdge;
722 for (screenEdge = 0; screenEdge < 4; screenEdge++) {
723 if (slidableEdgesAdjacentToWindow & (1 << screenEdge)) {
724 float distanceMouseOutsideWindow = AISignedExteriorDistanceRect_edge_toPoint_(windowFrame, AIOppositeRectEdge_(screenEdge), mouseLocation);
725 if (distanceMouseOutsideWindow > WINDOW_SLIDING_MOUSE_DISTANCE_TOLERANCE)
726 shouldSlideOffScreen = YES;
730 /* Don't allow the window to slide off if the user is dragging
731 * This method is hacky and does not completely work. is there a way to detect if the mouse is down?
733 NSEventType currentEventType = [[NSApp currentEvent] type];
734 if (currentEventType == NSLeftMouseDragged ||
735 currentEventType == NSRightMouseDragged ||
736 currentEventType == NSOtherMouseDragged ||
737 currentEventType == NSPeriodic) {
738 shouldSlideOffScreen = NO;
741 return shouldSlideOffScreen;
744 // note: may be inaccurate when mouse is up against an edge
745 - (NSScreen *)screenForPoint:(NSPoint)point
747 NSScreen *pointScreen = nil;
749 NSEnumerator *screenEnumerator = [[NSScreen screens] objectEnumerator];
751 while ((screen = [screenEnumerator nextObject]) != nil) {
752 if (NSPointInRect(point, NSInsetRect([screen frame], -1, -1))) {
753 pointScreen = screen;
761 - (NSRect)squareRectWithCenter:(NSPoint)point sideLength:(float)sideLength
763 return NSMakeRect(point.x - sideLength*0.5f, point.y - sideLength*0.5f, sideLength, sideLength);
766 - (BOOL)pointIsInScreenCorner:(NSPoint)point
769 NSScreen *menubarScreen = [[NSScreen screens] objectAtIndex:0];
770 float menubarHeight = NSMaxY([menubarScreen frame]) - NSMaxY([menubarScreen visibleFrame]); // breaks if the dock is at the top of the screen (i.e. if the user is insane)
772 NSRect screenFrame = [[self screenForPoint:point] frame];
773 NSPoint lowerLeft = screenFrame.origin;
774 NSPoint upperRight = NSMakePoint(NSMaxX(screenFrame), NSMaxY(screenFrame));
775 NSPoint lowerRight = NSMakePoint(upperRight.x, lowerLeft.y);
776 NSPoint upperLeft = NSMakePoint(lowerLeft.x, upperRight.y);
778 float sideLength = menubarHeight * 2.0f;
779 inCorner = (NSPointInRect(point, [self squareRectWithCenter:lowerLeft sideLength:sideLength])
780 || NSPointInRect(point, [self squareRectWithCenter:lowerRight sideLength:sideLength])
781 || NSPointInRect(point, [self squareRectWithCenter:upperLeft sideLength:sideLength])
782 || NSPointInRect(point, [self squareRectWithCenter:upperRight sideLength:sideLength]));
787 // YES if the mouse is against all edges of the screen where we previously slid the window and not in a corner.
788 // This means that this method will never return YES of the cl is slid into a corner.
789 - (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy
791 BOOL mouseNearSlideOffEdges = ([self windowSlidOffScreenEdgeMask] != AINoEdges);
793 NSPoint mouseLocation = [NSEvent mouseLocation];
795 NSRectEdge screenEdge;
796 for (screenEdge = 0; screenEdge < 4; screenEdge++) {
797 if (windowSlidOffScreenEdgeMask & (1 << screenEdge)) {
798 float mouseOutsideSlideBoundaryRectDistance = AISignedExteriorDistanceRect_edge_toPoint_(screenSlideBoundaryRect,
801 if(mouseOutsideSlideBoundaryRectDistance < -MOUSE_EDGE_SLIDE_ON_DISTANCE) {
802 mouseNearSlideOffEdges = NO;
807 return mouseNearSlideOffEdges && ![self pointIsInScreenCorner:mouseLocation];
810 #pragma mark Dock-like hiding
812 - (NSScreen *)windowLastScreen
814 return windowLastScreen;
817 - (BOOL)animationShouldStart:(NSAnimation *)animation
819 //Whenever an animation starts, we should be using the normal shadow setting
820 [[self window] setHasShadow:listHasShadow];
822 //Don't let docking interfere with the animation
823 if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
824 [(id)[self window] setDockingEnabled:NO];
826 if (windowSlidOffScreenEdgeMask == AINoEdges) {
827 [[self window] setAlphaValue:previousAlpha];
833 - (void)animationDidEnd:(NSAnimation*)animation
835 //Restore docking behavior
836 if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
837 [(id)[self window] setDockingEnabled:YES];
839 if (windowSlidOffScreenEdgeMask == AINoEdges) {
840 /* When the window is offscreen, there are no constraints on its size, for example it will grow downwards as much as
841 * it needs to to accomodate new rows. Now that it's onscreen, there are constraints.
843 [contactListController contactListDesiredSizeChanged];
846 //Offscreen windows should be told not to cast a shadow
847 [[self window] setHasShadow:NO];
849 previousAlpha = [[self window] alphaValue];
850 [[self window] setAlphaValue:0.0];
853 [windowAnimation release]; windowAnimation = nil;
856 - (BOOL)keepListOnScreenWhenSliding
862 * @brief Slide the window to a given point
864 * windowSlidOffScreenEdgeMask must already be set to the resulting offscreen mask (or 0 if the window is sliding on screen)
866 * A standard window (titlebar window) will crash if told to setFrame completely offscreen. Also, using our own movement we can more precisely
867 * control the movement speed and acceleration.
869 - (void)slideWindowToPoint:(NSPoint)targetPoint
871 NSWindow *myWindow = [self window];
872 NSScreen *windowScreen;
874 windowScreen = [myWindow screen];
875 if (!windowScreen) windowScreen = [self windowLastScreen];
876 if (!windowScreen) windowScreen = [NSScreen mainScreen];
878 NSRect frame = [myWindow frame];
879 float yOff = (targetPoint.y + NSHeight(frame)) - NSMaxY([windowScreen frame]);
880 if (windowScreen == [[NSScreen screens] objectAtIndex:0]) yOff -= [NSMenuView menuBarHeight];
881 if (yOff > 0) targetPoint.y -= yOff;
883 frame.origin = targetPoint;
885 if ((windowSlidOffScreenEdgeMask != AINoEdges) &&
886 [self keepListOnScreenWhenSliding]) {
887 switch (windowSlidOffScreenEdgeMask) {
901 //We'll never get here
906 if (windowAnimation) {
907 [windowAnimation stopAnimation];
908 [windowAnimation release];
911 windowAnimation = [[NSViewAnimation alloc] initWithViewAnimations:
912 [NSArray arrayWithObject:
913 [NSDictionary dictionaryWithObjectsAndKeys:
914 myWindow, NSViewAnimationTargetKey,
915 [NSValue valueWithRect:frame], NSViewAnimationEndFrameKey,
917 [windowAnimation setFrameRate:0.0];
918 [windowAnimation setDuration:0.25];
919 [windowAnimation setDelegate:self];
920 [windowAnimation setAnimationBlockingMode:NSAnimationNonblocking];
921 [windowAnimation startAnimation];
924 - (void)moveWindowToPoint:(NSPoint)inOrigin
926 [[self window] setFrameOrigin:inOrigin];
928 if (windowSlidOffScreenEdgeMask == AINoEdges) {
929 /* When the window is offscreen, there are no constraints on its size, for example it will grow downwards as much as
930 * it needs to to accomodate new rows. Now that it's onscreen, there are constraints.
932 [contactListController contactListDesiredSizeChanged];
933 [[self window] setAlphaValue:previousAlpha];
938 * @brief Find the mask specifying what edges are potentially slidable for our window
940 * @result AIRectEdgeMask, which is 0 if no edges are slidable
942 - (AIRectEdgeMask)slidableEdgesAdjacentToWindow
944 AIRectEdgeMask slidableEdges = 0;
946 NSWindow *window = [self window];
947 NSRect windowFrame = [window frame];
950 for (edge = 0; edge < 4; edge++) {
951 if ((SLIDE_ALLOWED_RECT_EDGE_MASK & (1 << edge)) &&
952 (AIRectIsAligned_edge_toRect_edge_tolerance_(windowFrame,
954 screenSlideBoundaryRect,
956 WINDOW_ALIGNMENT_TOLERANCE))) {
957 slidableEdges |= (1 << edge);
961 return slidableEdges;
964 - (void)slideWindowOffScreenEdges:(AIRectEdgeMask)rectEdgeMask
967 NSRect newWindowFrame;
970 if (rectEdgeMask == AINoEdges)
973 window = [self window];
974 newWindowFrame = [window frame];
976 [self setSavedFrame:newWindowFrame];
978 [windowLastScreen release];
979 windowLastScreen = [[window screen] retain];
981 for (edge = 0; edge < 4; edge++) {
982 if (rectEdgeMask & (1 << edge)) {
983 newWindowFrame = AIRectByAligningRect_edge_toRect_edge_(newWindowFrame,
984 AIOppositeRectEdge_(edge),
985 screenSlideBoundaryRect,
990 windowSlidOffScreenEdgeMask |= rectEdgeMask;
992 [self slideWindowToPoint:newWindowFrame.origin];
995 - (void)slideWindowOnScreenWithAnimation:(BOOL)animate
997 if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
998 NSWindow *window = [self window];
999 NSRect windowFrame = [window frame];
1001 if (!NSEqualRects(windowFrame, oldFrame)) {
1002 //Restore shadow and frame if we're appearing from having slid off-screen
1003 [window setHasShadow:[[[adium preferenceController] preferenceForKey:KEY_CL_WINDOW_HAS_SHADOW
1004 group:PREF_GROUP_CONTACT_LIST] boolValue]];
1005 [window orderFront:nil];
1007 windowSlidOffScreenEdgeMask = AINoEdges;
1010 [self slideWindowToPoint:oldFrame.origin];
1012 [self moveWindowToPoint:oldFrame.origin];
1015 [windowLastScreen release]; windowLastScreen = nil;
1020 - (void)slideWindowOnScreen
1022 [self slideWindowOnScreenWithAnimation:YES];
1025 - (void)setPreventHiding:(BOOL)newPreventHiding {
1026 preventHiding = newPreventHiding;
1029 - (void)endWindowSlidingDelay
1031 [self setPreventHiding:NO];
1034 - (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime
1036 [self setPreventHiding:YES];
1038 [NSObject cancelPreviousPerformRequestsWithTarget:self
1039 selector:@selector(endWindowSlidingDelay)
1041 [self performSelector:@selector(endWindowSlidingDelay)
1043 afterDelay:inDelayTime];
1046 - (AIRectEdgeMask)windowSlidOffScreenEdgeMask
1048 return windowSlidOffScreenEdgeMask;
1051 // Snap Groups Together------------------------------------------------------------------------------------------------
1052 #pragma mark Snap Groups Together
1055 * @brief If window did move and is not docked then snap it to other windows
1057 - (void)windowDidMove:(NSNotification *)notification
1059 BOOL suppressSnapping = [NSEvent shiftKey];
1061 attachToBottom = nil;
1063 if (windowSlidOffScreenEdgeMask == AINoEdges && !suppressSnapping)
1064 [self snapToOtherWindows];
1068 * @brief Captures mouse up event to check that if the window snapped underneath
1069 * another window they are merged together
1071 - (void)mouseUp:(NSEvent *)event {
1072 if (attachToBottom) {
1073 AIListGroup *from = (AIListGroup *)[self contactList];
1074 AIListGroup *to = (AIListGroup *)[attachToBottom contactList];
1076 [from moveAllGroupsFrom:from to:to];
1078 [[adium notificationCenter] postNotificationName:DetachedContactListIsEmpty
1081 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
1088 * @brief Snaps window to windows next to it
1090 - (void)snapToOtherWindows
1092 NSWindow *myWindow = [self window];
1093 NSArray *windows = [[NSApplication sharedApplication] windows];
1094 NSEnumerator *enumerator = [windows objectEnumerator];
1098 NSRect currentFrame = [myWindow frame];
1099 NSPoint suggested = currentFrame.origin;
1101 // Check to snap to each guide
1102 while ((window = [enumerator nextObject])) {
1103 // No snapping to itself and it must be within a snapping distance to other windows
1104 if ((window != myWindow) &&
1105 [window delegate] && [window isVisible] &&
1106 [[window delegate] conformsToProtocol:@protocol(AIInterfaceContainer)]) {
1107 /* Note: [window delegate] may be invalid if the window is in the middle of closing.
1108 * Checking if it's visible should hopefully cover that case.
1110 suggested = [self snapTo:window with:currentFrame saveTo:suggested];
1114 [[self window] setFrameOrigin:suggested];
1119 * @brief Check that window is inside snappable region of other window
1121 static BOOL isInRangeOfRect(NSRect sourceRect, NSRect targetRect)
1123 return NSIntersectsRect(NSInsetRect(sourceRect, -SNAP_DISTANCE, -SNAP_DISTANCE), targetRect);
1127 * @brief Check if points are close enough to be snapped together
1129 static BOOL canSnap(float a, float b)
1131 return (abs(a - b) <= SNAP_DISTANCE);
1134 - (NSPoint)snapTo:(NSWindow*)neighborWindow with:(NSRect)currentRect saveTo:(NSPoint)location{
1135 NSRect neighbor = [neighborWindow frame];
1136 NSPoint spacing = [self windowSpacing];
1137 unsigned overlap = 0;
1138 unsigned bottom = 0;
1140 if (!NSEqualRects(neighbor,currentRect) && isInRangeOfRect(currentRect, neighbor)) {
1142 if (canSnap(NSMaxX(currentRect), NSMinX(neighbor))) {
1143 location.x = NSMinX(neighbor) - NSWidth(currentRect) - spacing.x;
1144 } else if (canSnap(NSMinX(currentRect), NSMaxX(neighbor))) {
1145 location.x = NSMaxX(neighbor) + spacing.x;
1146 } else if (canSnap(NSMinX(currentRect), NSMinX(neighbor))) {
1147 location.x = NSMinX(neighbor);
1153 if (canSnap(NSMaxY(neighbor), NSMaxY(currentRect))) {
1154 location.y = NSMaxY(neighbor) - NSHeight(currentRect);
1156 } else if (canSnap(NSMinY(neighbor), NSMaxY(currentRect))) {
1157 location.y = NSMinY(neighbor) - NSHeight(currentRect) - spacing.y;
1159 } else if (canSnap(NSMaxY(neighbor), NSMinY(currentRect))) {
1160 location.y = NSMaxY(neighbor) + spacing.y;
1161 } else if (canSnap(NSMinY(neighbor), NSMinY(currentRect))) {
1162 location.y = NSMinY(neighbor);
1168 // If we snapped on top of neighbor
1170 return currentRect.origin;
1172 // Save window that we could possible attach to
1174 attachToBottom = [neighborWindow delegate];
1181 * @brief Gets space that windows should be apart by based on current window style
1183 - (NSPoint)windowSpacing {
1184 AIContactListWindowStyle style = [[[adium preferenceController] preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE
1185 group:PREF_GROUP_APPEARANCE] intValue];
1186 int space = [[[adium preferenceController] preferenceForKey:@"Group Top Spacing"
1187 group:@"List Layout"] intValue];
1190 case AIContactListWindowStyleStandard:
1191 case AIContactListWindowStyleBorderless:
1192 return NSMakePoint(0,0);
1193 case AIContactListWindowStyleGroupBubbles:
1194 case AIContactListWindowStyleContactBubbles:
1195 case AIContactListWindowStyleContactBubbles_Fitted:
1196 return NSMakePoint(space,space-WINDOW_ALIGNMENT_TOLERANCE);
1198 return NSMakePoint(0,0);