We need a b8 before we can release since we've had some significant changes since...
[adiumx.git] / Source / AIListWindowController.m
blob21d194a1e79b8df8a3d85271ce9d7a1b2275af27
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
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;
66 @end
68 @implementation AIListWindowController
70 + (void)initialize
72         if ([self isEqual:[AIListWindowController class]]) {
73                 [[NSNotificationCenter defaultCenter] addObserver:self
74                                                                                                  selector:@selector(updateScreenSlideBoundaryRect:) 
75                                                                                                          name:NSApplicationDidChangeScreenParametersNotification 
76                                                                                                    object:nil];
77                 
78                 [self updateScreenSlideBoundaryRect:nil];
79         }
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]])) {
96                 preventHiding = NO;
97                 previousAlpha = 0;
98                 [self setContactList:contactList];
99         }
100         
101         return self;    
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];
124         }
127 //Our window nib name
128 + (NSString *)nibName
130     return @"";
133 - (Class)listControllerClass
135         return [AIListController class];
138 - (void)dealloc
140         [[NSNotificationCenter defaultCenter] removeObserver:self];
142         [contactListController close];
143         [windowLastScreen release];
145         [super dealloc];
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
157 {       
158         contactListController = [[[self listControllerClass] alloc] initWithContactList:[self contactList]
159                                                                                                                                           inOutlineView:contactListView
160                                                                                                                                            inScrollView:scrollView_contactList 
161                                                                                                                                                    delegate:self];
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 
179                                                                                            object:nil];
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];
188         
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];
192         
193         [[NSNotificationCenter defaultCenter] addObserver:self
194                                                                                          selector:@selector(applicationDidUnhide:) 
195                                                                                                  name:NSApplicationDidUnhideNotification 
196                                                                                            object:nil];
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];
208                 
209                 //Then move it back on screen so that we'll save the proper position in -[AIWindowController windowWillClose:]
210                 [self slideWindowOnScreenWithAnimation:NO];
211         }
213         [super windowWillClose:notification];
215         //Invalidate the dock-like hiding timer
216         [slideWindowIfNeededTimer invalidate]; [slideWindowIfNeededTimer release];
218     //Stop observing
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)
231         int                             level;
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;
238         }
239         
240         return level;
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
248         }
251 //Preferences have changed
252 - (void)preferencesChangedForGroup:(NSString *)group 
253                                                            key:(NSString *)key
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];
262                 
263                 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
265                 listHasShadow = [[prefDict objectForKey:KEY_CL_WINDOW_HAS_SHADOW] boolValue];
266                 [[self window] setHasShadow:listHasShadow];
267                 
268                 windowHidingStyle = [[prefDict objectForKey:KEY_CL_WINDOW_HIDING_STYLE] intValue];
269                 slideOnlyInBackground = [[prefDict objectForKey:KEY_CL_SLIDE_ONLY_IN_BACKGROUND] boolValue];
270                 
271                 [[self window] setHidesOnDeactivate:(windowHidingStyle == AIContactListWindowHidingStyleBackground)];
272                 
273                 if ([[NSApplication sharedApplication] isOnLeopardOrBetter]) {
274                         if (windowHidingStyle == AIContactListWindowHidingStyleSliding || [[prefDict objectForKey:KEY_CL_ALL_SPACES] boolValue])
275                                 [[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
276                         else
277                                 [[self window] setCollectionBehavior:NSWindowCollectionBehaviorDefault];
278                 }
280                 if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
281                         if (!slideWindowIfNeededTimer) {
282                                 slideWindowIfNeededTimer = [[NSTimer scheduledTimerWithTimeInterval:DOCK_HIDING_MOUSE_POLL_INTERVAL
283                                                                                                                                                          target:self
284                                                                                                                                                    selector:@selector(slideWindowIfNeeded:)
285                                                                                                                                                    userInfo:nil
286                                                                                                                                                         repeats:YES] retain];
287                         }
289                 } else if (slideWindowIfNeededTimer) {
290             [slideWindowIfNeededTimer invalidate];
291                         [slideWindowIfNeededTimer release]; slideWindowIfNeededTimer = nil;
292                 }
294                 [contactListController setShowTooltips:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS] boolValue]];
295                 [contactListController setShowTooltipsInBackground:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS_IN_BACKGROUND] boolValue]];
296     }
297         
298         //Auto-Resizing
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;
304                 
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;
309                 } else {
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;
314                         } else {
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;
318                         }
319                 }
320                 
321                 //Show the resize indicator if either or both of the autoresizing options is NO
322                 [[self window] setShowsResizeIndicator:!(autoResizeVertically && autoResizeHorizontally)];
323                 
324                 /*
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).
327                  */
328                 NSSize  thisMinimumSize = minWindowSize;
329                 NSSize  thisMaximumSize = NSMakeSize(maxWindowWidth, 10000);
330                 NSRect  currentFrame = [[self window] frame];
331                 
332                 if (forcedWindowWidth != -1) {
333                         /*
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.
336                          */
337                         if (!(autoResizeVertically || autoResizeHorizontally)) {
338                                 thisMinimumSize.width = forcedWindowWidth;
339                                 [[self window] setFrame:NSMakeRect(currentFrame.origin.x,currentFrame.origin.y,forcedWindowWidth,currentFrame.size.height) 
340                                                                 display:YES
341                                                                 animate:NO];
342                         }
343                 }
344                 
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;
349                 }
350                 
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;                        
355                 }
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
359                  * size.
360                  */
361                 [contactListView setMinimumDesiredWidth:((windowStyle == AIContactListWindowStyleStandard) ? 175 : 0)];
363                 [[self window] setMinSize:thisMinimumSize];
364                 [[self window] setMaxSize:thisMaximumSize];
365                 
366                 [contactListController setAutoresizeHorizontally:autoResizeHorizontally];
367                 [contactListController setAutoresizeVertically:autoResizeVertically];
368                 [contactListController setForcedWindowWidth:forcedWindowWidth];
369                 [contactListController setMaxWindowWidth:maxWindowWidth];
370                 
371                 [contactListController contactListDesiredSizeChanged];
372                 
373                 if (!firstTime) {
374                         shouldRevealWindowAndDelaySliding = YES;
375                 }
376         }
378         //Window opacity
379         if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
380                 float opacity = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_OPACITY] floatValue];            
381                 [contactListController setBackgroundOpacity:opacity];
382                 
383                 if (!firstTime) {
384                         shouldRevealWindowAndDelaySliding = YES;
385                 }
386         }
387         
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];
395                 //Layout only
396                 if (groupLayout) {
397                         int iconSize = [[layoutDict objectForKey:KEY_LIST_LAYOUT_USER_ICON_SIZE] intValue];
398                         [AIUserIcons setListUserIconSize:NSMakeSize(iconSize,iconSize)];
399                 }
400                         
401                 //Theme only
402                 if (groupTheme || firstTime) {
403                         NSString                *imagePath = [themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_PATH];
404                         
405                         //Background Image
406                         if (imagePath && [imagePath length] && [[themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_ENABLED] boolValue]) {
407                                 [contactListView setBackgroundImage:[[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease]];
408                         } else {
409                                 [contactListView setBackgroundImage:nil];
410                         }
411                 }
412                 
413                 //Both layout and theme
414                 [contactListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
415                 
416                 if (!firstTime) {
417                         shouldRevealWindowAndDelaySliding = YES;
418                 }
419         }
421         if (shouldRevealWindowAndDelaySliding) {
422                 [self delayWindowSlidingForInterval:2];
423                 [self slideWindowOnScreenWithAnimation:NO];
425         } else {
426                 //Do a slide immediately if needed (to display as per our new preferneces)
427                 [self slideWindowIfNeeded:nil];
428                 
429         }
432 - (IBAction)performDefaultActionOnSelectedObject:(AIListObject *)selectedObject sender:(NSOutlineView *)sender
433 {       
434     if ([selectedObject isKindOfClass:[AIListGroup class]]) {
435         //Expand or collapse the group
436         if ([sender isItemExpanded:selectedObject]) {
437             [sender collapseItem:selectedObject];
438         } else {
439             [sender expandItem:selectedObject];
440         }
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]];
455     } 
456                 
457                 
460 - (BOOL) canCustomizeToolbar
462         return NO;
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.
473     [self retain];
475     if ([self windowShouldClose:nil]) {
476         [[self window] close];
477     }
479     [self release];
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];
508         
509         NSWindow        *window = [self window];
510         
511         if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
512                 [self slideWindowOnScreenWithAnimation:NO];
513         }
514         
515         windowSlidOffScreenEdgeMask = AINoEdges;
516         
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];
523         }
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
533         oldFrame = 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];
550         
551         NSScreen        *windowScreen = [window screen];
552         if (!windowScreen) {
553                 if ([[NSScreen screens] containsObject:windowLastScreen]) {
554                         windowScreen = windowLastScreen;
555                 } else {
556                         [windowLastScreen release]; windowLastScreen = nil;
557                         windowScreen = [NSScreen mainScreen];
558                 }
559         }
561         NSRect newScreenFrame = [windowScreen frame];
562         
563         if ([[NSScreen screens] count] &&
564                 (windowScreen == [[NSScreen screens] objectAtIndex:0])) {
565                         newScreenFrame.size.height -= [NSMenuView menuBarHeight];
566         }
568         NSRect listFrame = [window frame];
569         
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));
572         
573         [self delayWindowSlidingForInterval:2];
574         [self slideWindowOnScreenWithAnimation:NO];
576         [contactListController contactListDesiredSizeChanged];
578         currentScreen = [window screen];
579         currentScreenFrame = newScreenFrame;
580         [self setSavedFrame:[window frame]];
583 // Printing
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) 
595  */
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];
601         
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]);
609                 }
610         }
614  * @brief Adium unhid
616  * If the contact list is open but not visible when we unhide, we should always display it; it should not, however, steal focus.
617  */
618 - (void)applicationDidUnhide:(NSNotification *)notification
620         if (![[self window] isVisible]) {
621                 [self showWindowInFrontIfAllowed:NO];
622         }
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;
637                 }
639                 [self slideWindowOnScreen];
641         } else if ([self shouldSlideWindowOffScreen]) {
642                 AIRectEdgeMask adjacentEdges = [self slidableEdgesAdjacentToWindow];
644         if (adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask)) {
645             [self slideWindowOffScreenEdges:(adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask))];
646                 } else {
647             [self slideWindowOffScreenEdges:adjacentEdges];
648                 }
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.
652                  */
653                 if (overrodeWindowLevel &&
654                         windowHidingStyle == AIContactListWindowHidingStyleSliding) {
655                         [self setWindowLevel:kCGBackstopMenuLevel];
656                         overrodeWindowLevel = YES;
657                 }
658                 
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.
666                  */
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;
671         }
674 - (BOOL)shouldSlideWindowOnScreen
676         BOOL shouldSlide = NO;
677         
678         if (([self windowSlidOffScreenEdgeMask] != AINoEdges) &&
679                 ![NSApp isHidden]) {
680                 if (slideOnlyInBackground && [NSApp isActive]) {
681                         //We only slide while in the background, and the app is not in the background. Slide on screen.
682                         shouldSlide = YES;
684                 } else if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
685                         //Slide on screen if the mouse position indicates we should
686                         shouldSlide = [self shouldSlideWindowOnScreen_mousePositionStrategy];
687                 } else {
688                         //It's slid off-screen... and it's not supposed to be sliding at all.  Slide back on screen!
689                         shouldSlide = YES;
690                 }
691         }
693         return shouldSlide;
696 - (BOOL)shouldSlideWindowOffScreen
698         BOOL shouldSlide = NO;
699         
700         if ((windowHidingStyle == AIContactListWindowHidingStyleSliding) &&
701                 !preventHiding &&
702                 ([self windowSlidOffScreenEdgeMask] == AINoEdges) &&
703                 (!(slideOnlyInBackground && [NSApp isActive]))) {
704                 shouldSlide = [self shouldSlideWindowOffScreen_mousePositionStrategy];
705         }
707         return shouldSlide;
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;
715         
716         NSWindow *window = [self window];
717         NSRect windowFrame = [window frame];
718         NSPoint mouseLocation = [NSEvent mouseLocation];
719         
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;
727                 }
728         }
729         
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?
732          */
733         NSEventType currentEventType = [[NSApp currentEvent] type];
734         if (currentEventType == NSLeftMouseDragged ||
735                 currentEventType == NSRightMouseDragged ||
736                 currentEventType == NSOtherMouseDragged ||
737                 currentEventType == NSPeriodic) {
738                 shouldSlideOffScreen = NO;
739         }       
740         
741         return shouldSlideOffScreen;
744 // note: may be inaccurate when mouse is up against an edge 
745 - (NSScreen *)screenForPoint:(NSPoint)point
747         NSScreen *pointScreen = nil;
748         
749         NSEnumerator *screenEnumerator = [[NSScreen screens] objectEnumerator];
750         NSScreen *screen;
751         while ((screen = [screenEnumerator nextObject]) != nil) {
752                 if (NSPointInRect(point, NSInsetRect([screen frame], -1, -1))) {
753                         pointScreen = screen;
754                         break;
755                 }               
756         }
757         
758         return pointScreen;
759 }       
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
768         BOOL inCorner = NO;
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)
771         
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);
777         
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]));
783         
784         return inCorner;
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);
792         
793         NSPoint mouseLocation = [NSEvent mouseLocation];
794         
795         NSRectEdge screenEdge;
796         for (screenEdge = 0; screenEdge < 4; screenEdge++) {
797                 if (windowSlidOffScreenEdgeMask & (1 << screenEdge)) {
798                         float mouseOutsideSlideBoundaryRectDistance = AISignedExteriorDistanceRect_edge_toPoint_(screenSlideBoundaryRect,
799                                                                                                                                                                                                          screenEdge,
800                                                                                                                                                                                                          mouseLocation);
801                         if(mouseOutsideSlideBoundaryRectDistance < -MOUSE_EDGE_SLIDE_ON_DISTANCE) {
802                                 mouseNearSlideOffEdges = NO;
803                         }
804                 }
805         }
806         
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];
821         
822         //Don't let docking interfere with the animation
823         if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
824                 [(id)[self window] setDockingEnabled:NO];
825         
826         if (windowSlidOffScreenEdgeMask == AINoEdges) {
827                 [[self window] setAlphaValue:previousAlpha];
828         }
830         return YES;
833 - (void)animationDidEnd:(NSAnimation*)animation
835         //Restore docking behavior      
836         if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
837                 [(id)[self window] setDockingEnabled:YES];
838         
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.
842                  */
843                 [contactListController contactListDesiredSizeChanged];
845         } else {
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];
851         }
852         
853         [windowAnimation release]; windowAnimation = nil;
856 - (BOOL)keepListOnScreenWhenSliding
858         return NO;
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.
868  */
869 - (void)slideWindowToPoint:(NSPoint)targetPoint
870 {       
871         NSWindow                                *myWindow = [self window];
872         NSScreen                                *windowScreen;
874         windowScreen = [myWindow screen];
875         if (!windowScreen) windowScreen = [self windowLastScreen];
876         if (!windowScreen) windowScreen = [NSScreen mainScreen];
877         
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;
882         
883         frame.origin = targetPoint;
884         
885         if ((windowSlidOffScreenEdgeMask != AINoEdges) &&
886                 [self keepListOnScreenWhenSliding]) {
887                 switch (windowSlidOffScreenEdgeMask) {
888                         case AIMinXEdgeMask:
889                                 frame.origin.x += 1;
890                                 break;
891                         case AIMaxXEdgeMask:
892                                 frame.origin.x -= 1;
893                                 break;
894                         case AIMaxYEdgeMask:
895                                 frame.origin.y -= 1;
896                                 break;
897                         case AIMinYEdgeMask:
898                                 frame.origin.y += 1;
899                                 break;
900                         case AINoEdges:
901                                 //We'll never get here
902                                 break;
903                 }
904         }
905         
906         if (windowAnimation) {
907                 [windowAnimation stopAnimation];
908                 [windowAnimation release];
909         }
911         windowAnimation = [[NSViewAnimation alloc] initWithViewAnimations:
912                 [NSArray arrayWithObject:
913                         [NSDictionary dictionaryWithObjectsAndKeys:
914                                 myWindow, NSViewAnimationTargetKey,
915                                 [NSValue valueWithRect:frame], NSViewAnimationEndFrameKey,
916                                 nil]]];
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.
931                 */
932                 [contactListController contactListDesiredSizeChanged];
933                 [[self window] setAlphaValue:previousAlpha];
934         }
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
941  */
942 - (AIRectEdgeMask)slidableEdgesAdjacentToWindow
944         AIRectEdgeMask slidableEdges = 0;
946         NSWindow *window = [self window];
947         NSRect windowFrame = [window frame];
948         
949         NSRectEdge edge;
950         for (edge = 0; edge < 4; edge++) {
951                 if ((SLIDE_ALLOWED_RECT_EDGE_MASK & (1 << edge)) &&
952                         (AIRectIsAligned_edge_toRect_edge_tolerance_(windowFrame,
953                                                                                                                  edge,
954                                                                                                                  screenSlideBoundaryRect,
955                                                                                                                  edge,
956                                                                                                                  WINDOW_ALIGNMENT_TOLERANCE))) { 
957                         slidableEdges |= (1 << edge);
958                 }
959         }
960         
961         return slidableEdges;
964 - (void)slideWindowOffScreenEdges:(AIRectEdgeMask)rectEdgeMask
966         NSWindow        *window;
967         NSRect          newWindowFrame;
968         NSRectEdge      edge;
970         if (rectEdgeMask == AINoEdges)
971                 return;
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,
986                                                                                                                                         edge);
987                 }
988         }
990         windowSlidOffScreenEdgeMask |= rectEdgeMask;
991                 
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];
1000                 
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]; 
1006                         
1007                         windowSlidOffScreenEdgeMask = AINoEdges;
1008                         
1009                         if (animate) {
1010                                 [self slideWindowToPoint:oldFrame.origin];
1011                         } else {
1012                                 [self moveWindowToPoint:oldFrame.origin];
1013                         }
1014                         
1015                         [windowLastScreen release];     windowLastScreen = nil;
1016                 }
1017         }
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];
1037         
1038         [NSObject cancelPreviousPerformRequestsWithTarget:self
1039                                                                                          selector:@selector(endWindowSlidingDelay)
1040                                                                                            object:nil];
1041         [self performSelector:@selector(endWindowSlidingDelay)
1042                            withObject:nil
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
1056  */
1057 - (void)windowDidMove:(NSNotification *)notification
1059         BOOL suppressSnapping = [NSEvent shiftKey];
1061         attachToBottom = nil;
1062         
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
1070  */
1071 - (void)mouseUp:(NSEvent *)event {
1072         if (attachToBottom) {
1073                 AIListGroup *from = (AIListGroup *)[self contactList];
1074                 AIListGroup *to = (AIListGroup *)[attachToBottom contactList];
1075                 
1076                 [from moveAllGroupsFrom:from to:to];
1077                 
1078                 [[adium notificationCenter] postNotificationName:DetachedContactListIsEmpty
1079                                                                                                   object:from
1080                                                                                                 userInfo:nil];
1081                 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
1082                                                                                                   object:to
1083                                                                                                 userInfo:nil]; 
1084         }
1088  * @brief Snaps window to windows next to it
1089  */
1090 - (void)snapToOtherWindows
1091 {       
1092         NSWindow *myWindow = [self window];
1093         NSArray *windows = [[NSApplication sharedApplication] windows];
1094         NSEnumerator *enumerator = [windows objectEnumerator];
1095         
1096         NSWindow *window;
1097         
1098         NSRect currentFrame = [myWindow frame];
1099         NSPoint suggested = currentFrame.origin;
1100         
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 delegate] conformsToProtocol:@protocol(AIInterfaceContainer)]) {
1106                         suggested = [self snapTo:window with:currentFrame saveTo:suggested];
1107                 }
1108         }
1110         [[self window] setFrameOrigin:suggested];
1115  * @brief Check that window is inside snappable region of other window
1116  */
1117 static BOOL isInRangeOfRect(NSRect sourceRect, NSRect targetRect)
1119         return NSIntersectsRect(NSInsetRect(sourceRect, -SNAP_DISTANCE, -SNAP_DISTANCE), targetRect);
1123  * @brief Check if points are close enough to be snapped together
1124  */
1125 static BOOL canSnap(float a, float b)
1127         return (abs(a - b) <= SNAP_DISTANCE);
1130 - (NSPoint)snapTo:(NSWindow*)neighborWindow with:(NSRect)currentRect saveTo:(NSPoint)location{
1131         NSRect neighbor = [neighborWindow frame];
1132         NSPoint spacing = [self windowSpacing];
1133         unsigned overlap = 0;
1134         unsigned bottom = 0;
1135         
1136         if (!NSEqualRects(neighbor,currentRect) && isInRangeOfRect(currentRect, neighbor)) {
1137                 // X Snapping
1138                 if (canSnap(NSMaxX(currentRect), NSMinX(neighbor))) {
1139                         location.x = NSMinX(neighbor) - NSWidth(currentRect) - spacing.x;
1140                 } else if (canSnap(NSMinX(currentRect), NSMaxX(neighbor))) {
1141                         location.x = NSMaxX(neighbor) + spacing.x;
1142                 } else if (canSnap(NSMinX(currentRect), NSMinX(neighbor))) {
1143                         location.x = NSMinX(neighbor);
1144                         overlap++;
1145                         bottom++;
1146                 }
1147                 
1148                 // Y Snapping
1149                 if (canSnap(NSMaxY(neighbor), NSMaxY(currentRect))) {
1150                         location.y = NSMaxY(neighbor) - NSHeight(currentRect);
1151                         overlap++;
1152                 } else if (canSnap(NSMinY(neighbor), NSMaxY(currentRect))) {
1153                         location.y = NSMinY(neighbor) - NSHeight(currentRect) - spacing.y;
1154                         bottom++;
1155                 } else if (canSnap(NSMaxY(neighbor), NSMinY(currentRect))) {
1156                         location.y = NSMaxY(neighbor) + spacing.y;
1157                 } else if (canSnap(NSMinY(neighbor), NSMinY(currentRect))) {
1158                         location.y = NSMinY(neighbor);
1159                         overlap++;
1160                 }
1161                 
1162         }       
1163         
1164         // If we snapped on top of neighbor
1165         if (overlap == 2)
1166                 return currentRect.origin;
1168         // Save window that we could possible attach to
1169         if (bottom == 2)
1170                 attachToBottom = [neighborWindow delegate];
1171         
1172         return location;
1177  * @brief Gets space that windows should be apart by based on current window style
1178  */
1179 - (NSPoint)windowSpacing {
1180         AIContactListWindowStyle style = [[[adium preferenceController] preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE
1181                                                                                                                   group:PREF_GROUP_APPEARANCE] intValue];
1182         int space = [[[adium preferenceController] preferenceForKey:@"Group Top Spacing" 
1183                                                                                                                   group:@"List Layout"] intValue];
1184         
1185         switch (style) {
1186                 case AIContactListWindowStyleStandard:
1187                 case AIContactListWindowStyleBorderless:
1188                         return NSMakePoint(0,0);
1189                 case AIContactListWindowStyleGroupBubbles:
1190                 case AIContactListWindowStyleContactBubbles:
1191                 case AIContactListWindowStyleContactBubbles_Fitted:
1192                         return NSMakePoint(space,space-WINDOW_ALIGNMENT_TOLERANCE);
1193         }
1194         return NSMakePoint(0,0);
1197 @end