Source/AIListWindowController.m
author Evan Schoenberg
Mon Jul 27 16:46:17 2009 -0500 (2009-07-27)
changeset 2558 037f9ce70de2
parent 2209 8376f14337ff
child 2849 63a5052fc169
permissions -rw-r--r--
Don't let the contact list's outline view go to 0 pixels high when showing the filter bar. This fixes a filter bar animation occurring before the contact list is populated. Fixes #12214
     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  */
    16 
    17 #import "AIListWindowController.h"
    18 
    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/AIDockControllerProtocol.h>
    25 #import <AIUtilities/AIWindowAdditions.h>
    26 #import <AIUtilities/AIFunctions.h>
    27 #import <AIUtilities/AIWindowControllerAdditions.h>
    28 #import <AIUtilities/AIApplicationAdditions.h>
    29 #import <AIUtilities/AIImageAdditions.h>
    30 #import <AIUtilities/AIOutlineViewAdditions.h>
    31 #import <Adium/AIListBookmark.h>
    32 #import <Adium/AIListContact.h>
    33 #import <Adium/AIListGroup.h>
    34 #import <Adium/AIListObject.h>
    35 #import <Adium/AIProxyListObject.h>
    36 #import <Adium/AIUserIcons.h>
    37 #import <AIUtilities/AIDockingWindow.h>
    38 #import <AIUtilities/AIEventAdditions.h>
    39 #import <Adium/AIContactList.h>
    40 #import <Adium/AIContactHidingController.h>
    41 
    42 #import "AISearchFieldCell.h"
    43 
    44 #define	KEY_HIDE_CONTACT_LIST_GROUPS			@"Hide Contact List Groups"
    45 
    46 #define SLIDE_ALLOWED_RECT_EDGE_MASK			(AIMinXEdgeMask | AIMaxXEdgeMask) /* Screen edges on which sliding is allowde */
    47 #define DOCK_HIDING_MOUSE_POLL_INTERVAL			0.1 /* Interval at which to check the mouse position for sliding */
    48 #define	WINDOW_SLIDING_DELAY					0.2 /* Time after the mouse is in the right place before the window slides on screen */
    49 #define WINDOW_ALIGNMENT_TOLERANCE				2.0f /* Threshold distance far the window from an edge to be considered on it */
    50 #define MOUSE_EDGE_SLIDE_ON_DISTANCE			1.1f /* ??? */
    51 #define WINDOW_SLIDING_MOUSE_DISTANCE_TOLERANCE 3.0f /* Distance the mouse must be from the window's frame to be considered outside it */
    52 
    53 #define SNAP_DISTANCE							15.0 /* Distance beween one window's edge and another's at which they should snap together */
    54 
    55 @interface AIListWindowController ()
    56 - (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList;
    57 + (NSString *)nibName;
    58 + (void)updateScreenSlideBoundaryRect:(id)sender;
    59 - (BOOL)shouldSlideWindowOffScreen_mousePositionStrategy;
    60 - (void)slideWindowIfNeeded:(id)sender;
    61 - (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy;
    62 - (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime;
    63 
    64 - (void)showFilterBarWithAnimation:(BOOL)flag;
    65 - (void)hideFilterBarWithAnimation:(BOOL)flag;
    66 - (void)animateFilterBarWithDuration:(CGFloat)duration;
    67 @end
    68 
    69 @implementation AIListWindowController
    70 
    71 @synthesize windowAnimation, filterBarAnimation;
    72 
    73 static NSMutableDictionary *screenSlideBoundaryRectDictionary = nil;
    74 
    75 + (void)initialize
    76 {
    77 	if ([self isEqual:[AIListWindowController class]]) {
    78 		[[NSNotificationCenter defaultCenter] addObserver:self
    79 												 selector:@selector(updateScreenSlideBoundaryRect:) 
    80 													 name:NSApplicationDidChangeScreenParametersNotification 
    81 												   object:nil];
    82 		
    83 		[self updateScreenSlideBoundaryRect:nil];
    84 	}
    85 }
    86 
    87 + (AIListWindowController *)listWindowControllerForContactList:(AIListObject<AIContainingObject> *)contactList
    88 {
    89 	return [[[self alloc] initWithContactList:contactList] autorelease];
    90 }
    91 
    92 - (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList
    93 {
    94 	if ((self = [self initWithWindowNibName:[[self class] nibName]])) {
    95 		preventHiding = NO;
    96 		previousAlpha = 0;
    97 		
    98 		[NSBundle loadNibNamed:@"Filter Bar" owner:self];
    99 		
   100 		[self setContactList:contactList];
   101 	}
   102 	
   103 	return self;	
   104 }
   105 
   106 - (AIListObject<AIContainingObject> *)contactList
   107 {
   108 	return (contactListRoot ? contactListRoot : [contactListController contactList]);
   109 }
   110 
   111 - (AIListController *) listController
   112 {
   113 	return contactListController;
   114 }
   115 
   116 - (AIListOutlineView *)contactListView
   117 {
   118 	return contactListView;
   119 }
   120 
   121 - (void)setContactList:(AIListObject<AIContainingObject> *)inContactList
   122 {
   123 	if (inContactList != contactListRoot) {
   124 		[contactListRoot release];
   125 		contactListRoot = [inContactList retain];
   126 	}
   127 }
   128 
   129 //Our window nib name
   130 + (NSString *)nibName
   131 {
   132     return @"";
   133 }
   134 
   135 - (Class)listControllerClass
   136 {
   137 	return [AIListController class];
   138 }
   139 
   140 - (void)dealloc
   141 {
   142 	[searchField setDelegate:nil];
   143 	
   144 	[filterBarAnimation stopAnimation];
   145 	[filterBarAnimation setDelegate:nil];
   146 	self.filterBarAnimation = nil;
   147 	
   148 	[filterBarPreviouslySelected release];
   149 	
   150 	[[NSNotificationCenter defaultCenter] removeObserver:self];
   151 
   152 	[windowAnimation stopAnimation];
   153 	[windowAnimation setDelegate:nil];
   154 	self.windowAnimation = nil;
   155 	
   156 	[contactListController close];
   157 	[windowLastScreen release];
   158 
   159 	[super dealloc];
   160 }
   161 
   162 - (NSString *)adiumFrameAutosaveName
   163 {
   164 	AILogWithSignature(@"My autosave name is %@",[NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]]);
   165 	return [NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]];
   166 }
   167 
   168 //Setup the window after it has loaded
   169 - (void)windowDidLoad
   170 {	
   171 	contactListController = [[[self listControllerClass] alloc] initWithContactList:[self contactList]
   172 																	  inOutlineView:contactListView
   173 																	   inScrollView:scrollView_contactList 
   174 																		   delegate:self];
   175 
   176 	//super's windowDidLoad will restore our location, which is based upon the contactListRoot
   177 	[super windowDidLoad];
   178 
   179     //Exclude this window from the window menu (since we add it manually)
   180     [[self window] setExcludedFromWindowsMenu:YES];
   181 	[[self window] useOptimizedDrawing:YES];
   182 
   183 	minWindowSize = [[self window] minSize];
   184 	[contactListController setMinWindowSize:minWindowSize];
   185 
   186 	[[self window] setTitle:AILocalizedString(@"Contacts","Contact List window title")];
   187 
   188     //Watch for resolution and screen configuration changes
   189     [[NSNotificationCenter defaultCenter] addObserver:self
   190 											 selector:@selector(screenParametersChanged:) 
   191 												 name:NSApplicationDidChangeScreenParametersNotification 
   192 											   object:nil];
   193 	
   194 	// Filter bar	
   195 	filterBarExpandedGroups = NO;
   196 	filterBarIsVisible = NO;
   197 	filterBarShownAutomatically = NO;
   198 	self.filterBarAnimation = nil;
   199 	filterBarPreviouslySelected = nil;
   200 	[searchField setDelegate:self];
   201 	
   202 
   203 	//Show the contact list initially even if it is at a screen edge and supposed to slide out of view
   204 	[self delayWindowSlidingForInterval:5];
   205 
   206 	id<AIPreferenceController> preferenceController = adium.preferenceController;
   207     //Observe preference changes
   208 	[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST];
   209 	[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST_DISPLAY];
   210 	[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_APPEARANCE];
   211 	
   212 	//Preference code below assumes layout is done before theme.
   213 	[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_LAYOUT];
   214 	[preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_LIST_THEME];
   215 	
   216 	[[NSNotificationCenter defaultCenter] addObserver:self
   217 											 selector:@selector(applicationDidUnhide:) 
   218 												 name:NSApplicationDidUnhideNotification 
   219 											   object:nil];
   220 
   221 	//Substitute an otherwise identical copy of the search field for one of our class. We don't want to globally pose as class; we just want it here.
   222 	[NSKeyedArchiver setClassName:@"AISearchFieldCell" forClass:[NSSearchFieldCell class]];
   223 	[searchField setCell:[NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:[searchField cell]]]];	
   224 	[NSKeyedArchiver setClassName:@"NSSearchFieldCell" forClass:[NSSearchFieldCell class]];
   225 	
   226 	/* Get rid of the "x" button in the search field that would clear the search.
   227 	 * It conflicts with the other "x" button that hides the entire bar, and clearing a few characters is probably not necessary.
   228 	 */
   229 	[[searchField cell] setCancelButtonCell:nil];
   230 	
   231 	[[NSNotificationCenter defaultCenter] addObserver:self
   232 											 selector:@selector(windowDidResignMain:)
   233 												 name:NSWindowDidResignMainNotification
   234 											   object:[self window]];
   235 	
   236 	//Save our frame immediately for sliding purposes
   237 	[self setSavedFrame:[[self window] frame]];
   238 }
   239 
   240 //Close the contact list window
   241 - (void)windowWillClose:(NSNotification *)notification
   242 {
   243 	if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
   244 		//Hide the window while it's still off-screen
   245 		[[self window] setAlphaValue:0.0];
   246 		AILogWithSignature(@"Setting to alpha 0 while the window is offscreen");
   247 		
   248 		//Then move it back on screen so that we'll save the proper position in -[AIWindowController windowWillClose:]
   249 		[self slideWindowOnScreenWithAnimation:NO];
   250 	}
   251 	
   252 	// When closing the contact list while a search is in progress, reset visibility first.
   253 	if (![[searchField stringValue] isEqualToString:@""]) {
   254 		[searchField setStringValue:@""];
   255 		[self filterContacts:searchField];
   256 	}
   257 
   258 	[super windowWillClose:notification];
   259 
   260 	//Invalidate the dock-like hiding timer
   261 	[slideWindowIfNeededTimer invalidate]; [slideWindowIfNeededTimer release];
   262 
   263     //Stop observing
   264 	[adium.preferenceController unregisterPreferenceObserver:self];
   265     [[NSNotificationCenter defaultCenter] removeObserver:self];
   266 	[[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
   267 
   268     //Tell the interface to unload our window
   269     NSNotificationCenter *adiumNotificationCenter = [NSNotificationCenter defaultCenter];
   270     [adiumNotificationCenter postNotificationName:Interface_ContactListDidResignMain object:self];
   271 	[adiumNotificationCenter postNotificationName:Interface_ContactListDidClose object:self];
   272 }
   273 
   274 NSInteger levelForAIWindowLevel(AIWindowLevel windowLevel)
   275 {
   276 	NSInteger				level;
   277 
   278 	switch (windowLevel) {
   279 		case AINormalWindowLevel: level = NSNormalWindowLevel; break;
   280 		case AIFloatingWindowLevel: level = NSFloatingWindowLevel; break;
   281 		case AIDesktopWindowLevel: level = kCGBackstopMenuLevel; break;
   282 		default: level = NSNormalWindowLevel; break;
   283 	}
   284 	
   285 	return level;
   286 }
   287 
   288 - (void)setWindowLevel:(NSInteger)level
   289 {
   290 	AILogWithSignature(@"Setting to %i", level);
   291 	[[self window] setLevel:level];
   292 }
   293 
   294 //Preferences have changed
   295 - (void)preferencesChangedForGroup:(NSString *)group 
   296 							   key:(NSString *)key
   297 							object:(AIListObject *)object 
   298 					preferenceDict:(NSDictionary *)prefDict 
   299 						 firstTime:(BOOL)firstTime
   300 {
   301 	BOOL shouldRevealWindowAndDelaySliding = NO;
   302 	
   303 	// Make sure we're not getting an object-specific update.
   304 	if (object != nil)
   305 			return;
   306 
   307     if ([group isEqualToString:PREF_GROUP_CONTACT_LIST]) {
   308 		windowLevel = [[prefDict objectForKey:KEY_CL_WINDOW_LEVEL] integerValue];
   309 		[self setWindowLevel:levelForAIWindowLevel(windowLevel)];
   310 
   311 		listHasShadow = [[prefDict objectForKey:KEY_CL_WINDOW_HAS_SHADOW] boolValue];
   312 		[[self window] setHasShadow:listHasShadow];
   313 		
   314 		windowHidingStyle = [[prefDict objectForKey:KEY_CL_WINDOW_HIDING_STYLE] integerValue];
   315 		slideOnlyInBackground = [[prefDict objectForKey:KEY_CL_SLIDE_ONLY_IN_BACKGROUND] boolValue];
   316 		
   317 		[[self window] setHidesOnDeactivate:(windowHidingStyle == AIContactListWindowHidingStyleBackground)];
   318 		
   319 	    showOnAllSpaces = [[prefDict objectForKey:KEY_CL_ALL_SPACES] boolValue];
   320 		[[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
   321 
   322 		if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
   323 			if (!slideWindowIfNeededTimer) {
   324 				slideWindowIfNeededTimer = [[NSTimer scheduledTimerWithTimeInterval:DOCK_HIDING_MOUSE_POLL_INTERVAL
   325 																			 target:self
   326 																		   selector:@selector(slideWindowIfNeeded:)
   327 																		   userInfo:nil
   328 																			repeats:YES] retain];
   329 			}
   330 
   331 		} else if (slideWindowIfNeededTimer) {
   332             [slideWindowIfNeededTimer invalidate];
   333 			[slideWindowIfNeededTimer release]; slideWindowIfNeededTimer = nil;
   334 		}
   335 
   336 		[contactListController setShowTooltips:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS] boolValue]];
   337 		[contactListController setShowTooltipsInBackground:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS_IN_BACKGROUND] boolValue]];
   338     }
   339 	
   340 	//Auto-Resizing
   341 	if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
   342 		AIContactListWindowStyle	windowStyle = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] integerValue];
   343 		BOOL	autoResizeHorizontally = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_AUTOSIZE] boolValue];
   344 		BOOL	autoResizeVertically = YES;
   345 		NSInteger		forcedWindowWidth, maxWindowWidth;
   346 		
   347 		//Determine how to handle vertical autosizing. AIAppearancePreferences must match this behavior for this to make sense.
   348 		switch (windowStyle) {
   349 			case AIContactListWindowStyleStandard:
   350 			case AIContactListWindowStyleBorderless:
   351 			case AIContactListWindowStyleGroupChat:
   352 				//Standard and borderless don't have to vertically autosize, but they might
   353 				autoResizeVertically = [[prefDict objectForKey:KEY_LIST_LAYOUT_VERTICAL_AUTOSIZE] boolValue];
   354 				break;
   355 			case AIContactListWindowStyleGroupBubbles:
   356 			case AIContactListWindowStyleContactBubbles:
   357 			case AIContactListWindowStyleContactBubbles_Fitted:
   358 				//The bubbles styles don't show a window; force them to autosize by leaving autoResizeVertically == YES
   359 				break;
   360 		}			
   361 
   362 		if (autoResizeHorizontally) {
   363 			//If autosizing, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH determines the maximum width; no forced width.
   364 			maxWindowWidth = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_WIDTH] integerValue];
   365 			forcedWindowWidth = -1;
   366 		} else {
   367 			if (windowStyle == AIContactListWindowStyleStandard/* || windowStyle == AIContactListWindowStyleBorderless*/) {
   368 				//In the non-transparent non-autosizing modes, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH has no meaning
   369 				maxWindowWidth = 10000;
   370 				forcedWindowWidth = -1;
   371 			} else {
   372 				//In the transparent non-autosizing modes, KEY_LIST_LAYOUT_HORIZONTAL_WIDTH determines the width of the window
   373 				forcedWindowWidth = [[prefDict objectForKey:KEY_LIST_LAYOUT_HORIZONTAL_WIDTH] integerValue];
   374 				maxWindowWidth = forcedWindowWidth;
   375 			}
   376 		}
   377 		
   378 		//Show the resize indicator if either or both of the autoresizing options is NO
   379 		[[self window] setShowsResizeIndicator:!(autoResizeVertically && autoResizeHorizontally)];
   380 		
   381 		/*
   382 		 Reset the minimum and maximum sizes in case [contactListController contactListDesiredSizeChanged]; doesn't cause a sizing change
   383 		 (and therefore the min and max sizes aren't set there).
   384 		 */
   385 		NSSize	thisMinimumSize = minWindowSize;
   386 		NSSize	thisMaximumSize = NSMakeSize(maxWindowWidth, 10000);
   387 		NSRect	currentFrame = [[self window] frame];
   388 		
   389 		if (forcedWindowWidth != -1) {
   390 			/*
   391 			 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
   392 			 contactListDesiredSizeChanged is called.
   393 			 */
   394 			if (!(autoResizeVertically || autoResizeHorizontally)) {
   395 				thisMinimumSize.width = forcedWindowWidth;
   396 				[[self window] setFrame:NSMakeRect(currentFrame.origin.x,currentFrame.origin.y,forcedWindowWidth,currentFrame.size.height) 
   397 								display:YES
   398 								animate:NO];
   399 			}
   400 		}
   401 		
   402 		//If vertically resizing, make the minimum and maximum heights the current height
   403 		if (autoResizeVertically) {
   404 			thisMinimumSize.height = currentFrame.size.height;
   405 			thisMaximumSize.height = currentFrame.size.height;
   406 		}
   407 		
   408 		//If horizontally resizing, make the minimum and maximum widths the current width
   409 		if (autoResizeHorizontally) {
   410 			thisMinimumSize.width = currentFrame.size.width;
   411 			thisMaximumSize.width = currentFrame.size.width;			
   412 		}
   413 
   414 		/* For a standard window, inform the contact list that, if asked, it wants to be 175 pixels or more.
   415 		 * A maximum width less than this can make the list autosize smaller, but if it has its druthers it'll be a sane
   416 		 * size.
   417 		 */
   418 		[contactListView setMinimumDesiredWidth:((windowStyle == AIContactListWindowStyleStandard) ? 175 : 0)];
   419 
   420 		[[self window] setMinSize:thisMinimumSize];
   421 		[[self window] setMaxSize:thisMaximumSize];
   422 		
   423 		contactListController.autoResizeHorizontally = autoResizeHorizontally;
   424 		contactListController.autoResizeVertically = autoResizeVertically;
   425 
   426 		[contactListController setForcedWindowWidth:forcedWindowWidth];
   427 		[contactListController setMaxWindowWidth:maxWindowWidth];
   428 		
   429 		[contactListController contactListDesiredSizeChanged];
   430 		
   431 		if (!firstTime) {
   432 			shouldRevealWindowAndDelaySliding = YES;
   433 		}
   434 	}
   435 
   436 	//Window opacity
   437 	if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
   438 		CGFloat opacity = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_OPACITY] doubleValue];		
   439 		[contactListController setBackgroundOpacity:opacity];
   440 
   441 		/*
   442 		 * If we're using fitted bubbles, we want the default behavior of the winodw, which is to respond to clicks on opaque areas
   443 		 * and ignore clicks on transparent areas.  If we're using any other style, we never want to ignore clicks.
   444 		 */
   445 		BOOL forceWindowToCatchMouseEvents = ([[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] integerValue] != AIContactListWindowStyleContactBubbles_Fitted);
   446 		if (forceWindowToCatchMouseEvents)
   447 			[[self window] setIgnoresMouseEvents:NO];
   448 
   449 		if (!firstTime) {
   450 			shouldRevealWindowAndDelaySliding = YES;
   451 		}
   452 	}
   453 	
   454 	if ([group isEqualToString:PREF_GROUP_CONTACT_LIST_DISPLAY]) {
   455 		[contactListController setUseContactListGroups:![[prefDict objectForKey:KEY_HIDE_CONTACT_LIST_GROUPS] boolValue]];
   456 	}
   457 	
   458 	//Layout and Theme ------------
   459 	BOOL groupLayout = ([group isEqualToString:PREF_GROUP_LIST_LAYOUT]);
   460 	BOOL groupTheme = ([group isEqualToString:PREF_GROUP_LIST_THEME]);
   461     if (groupLayout || (groupTheme && !firstTime)) { /* We don't want to execute this code twice when initializing */
   462 		NSDictionary	*layoutDict = [adium.preferenceController preferencesForGroup:PREF_GROUP_LIST_LAYOUT];
   463 		NSDictionary	*themeDict = [adium.preferenceController preferencesForGroup:PREF_GROUP_LIST_THEME];
   464 
   465 		//Layout only
   466 		if (groupLayout) {
   467 			NSInteger iconSize = [[layoutDict objectForKey:KEY_LIST_LAYOUT_USER_ICON_SIZE] integerValue];
   468 			[AIUserIcons setListUserIconSize:NSMakeSize(iconSize,iconSize)];
   469 		}
   470 			
   471 		//Theme only
   472 		if (groupTheme || firstTime) {
   473 			NSString		*imagePath = [themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_PATH];
   474 			
   475 			//Background Image
   476 			if (imagePath && [imagePath length] && [[themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_ENABLED] boolValue]) {
   477 				[contactListView setBackgroundImage:[[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease]];
   478 			} else {
   479 				[contactListView setBackgroundImage:nil];
   480 			}
   481 		}
   482 
   483 		EXTENDED_STATUS_STYLE statusStyle = [[layoutDict objectForKey:KEY_LIST_LAYOUT_EXTENDED_STATUS_STYLE] integerValue];
   484 		EXTENDED_STATUS_POSITION statusPosition = [[layoutDict objectForKey:KEY_LIST_LAYOUT_EXTENDED_STATUS_POSITION] integerValue];
   485 		contactListController.autoResizeHorizontallyWithIdleTime = 
   486 		 ((statusStyle == IDLE_ONLY || statusStyle == IDLE_AND_STATUS) &&
   487 		  (statusPosition == EXTENDED_STATUS_POSITION_BESIDE_NAME || statusPosition == EXTENDED_STATUS_POSITION_BOTH));
   488 		[contactListController contactListDesiredSizeChanged];
   489 
   490 		//Both layout and theme
   491 		[contactListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
   492 
   493 		if (!firstTime) {
   494 			shouldRevealWindowAndDelaySliding = YES;
   495 		}
   496 	}
   497 
   498 	if (shouldRevealWindowAndDelaySliding) {
   499 		[self delayWindowSlidingForInterval:2];
   500 		[self slideWindowOnScreenWithAnimation:NO];
   501 
   502 	} else {
   503 		//Do a slide immediately if needed (to display as per our new preferneces)
   504 		[self slideWindowIfNeeded:nil];
   505 		
   506 	}
   507 }
   508 
   509 - (IBAction)performDefaultActionOnSelectedObject:(AIListObject *)selectedObject sender:(NSOutlineView *)sender
   510 {	
   511     if ([selectedObject isKindOfClass:[AIListGroup class]]) {
   512         //Expand or collapse the group
   513 		for (AIProxyListObject *proxyObject in selectedObject.proxyObjects) {
   514 			if ([sender isItemExpanded:proxyObject]) {
   515 				[sender collapseItem:proxyObject];
   516 			} else {
   517 				[sender expandItem:proxyObject];					
   518 			}
   519 		}
   520 
   521 	} else if ([selectedObject isMemberOfClass:[AIListBookmark class]]) {
   522 		//Hide any tooltip the contactListController is currently showing
   523 		[contactListController hideTooltip];
   524 
   525 		[(AIListBookmark *)selectedObject openChat];
   526 
   527 	} else if ([selectedObject isKindOfClass:[AIListContact class]]) {
   528 		//Hide any tooltip the contactListController is currently showing
   529 		[contactListController hideTooltip];
   530 
   531 		//Open a new message with the contact
   532 		[adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)selectedObject
   533 																			onPreferredAccount:YES]];
   534     }
   535 }
   536 
   537 - (BOOL) canCustomizeToolbar
   538 {
   539 	return NO;
   540 }
   541 
   542 //Interface Container --------------------------------------------------------------------------------------------------
   543 #pragma mark Interface Container
   544 //Close this container
   545 - (void)close:(id)sender
   546 {
   547     //In response to windowShouldClose, the interface controller releases us.  At that point, no one would be retaining
   548 	//this instance of AIContactListWindowController, and we would be deallocated.  The call to [self window] will
   549 	//crash if we are deallocated.  A dirty, but functional fix is to temporarily retain ourself here.
   550     [self retain];
   551 
   552     if ([self windowShouldClose:nil]) {
   553         [[self window] close];
   554     }
   555 
   556     [self release];
   557 }
   558 
   559 - (void)makeActive:(id)sender
   560 {
   561 	[[self window] makeKeyAndOrderFront:self];
   562 }
   563 
   564 
   565 //Contact list brought to front
   566 - (void)windowDidBecomeKey:(NSNotification *)notification
   567 {
   568     [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactListDidBecomeMain object:self];
   569 }
   570 
   571 //Contact list sent back
   572 - (void)windowDidResignKey:(NSNotification *)notification
   573 {
   574     [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactListDidResignMain object:self];
   575 }
   576 
   577 - (void)showWindowInFrontIfAllowed:(BOOL)inFront
   578 {
   579 	//Always show for three seconds at least if we're told to show
   580 	[self delayWindowSlidingForInterval:3];
   581 
   582 	//Call super to actually do the showing
   583 	[super showWindowInFrontIfAllowed:inFront];
   584 	
   585 	NSWindow	*window = [self window];
   586 	
   587 	if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
   588 		[self slideWindowOnScreenWithAnimation:NO];
   589 	}
   590 	
   591 	windowSlidOffScreenEdgeMask = AINoEdges;
   592 	
   593 	currentScreen = [window screen];
   594 	currentScreenFrame = [currentScreen frame];
   595 
   596 	if ([[NSScreen screens] count] && 
   597 		(currentScreen == [[NSScreen screens] objectAtIndex:0])) {
   598 		currentScreenFrame.size.height -= [[NSApp mainMenu] menuBarHeight];
   599 	}
   600 
   601 	//Ensure the window is displaying at the proper level and exposé setting
   602 	[self setWindowLevel:levelForAIWindowLevel(windowLevel)];	
   603 }
   604 
   605 - (void)setSavedFrame:(NSRect)frame
   606 {
   607 	oldFrame = frame;
   608 }
   609 
   610 - (NSRect)savedFrame
   611 {
   612 	return oldFrame;
   613 }
   614 
   615 // Auto-resizing support ------------------------------------------------------------------------------------------------
   616 #pragma mark Auto-resizing support
   617 
   618 - (void)respondToScreenParametersChanged:(NSNotification *)notification
   619 {
   620 	NSWindow	*window = [self window];
   621 	
   622 	NSScreen	*windowScreen = [window screen];
   623 	if (!windowScreen) {
   624 		if ([[NSScreen screens] containsObject:windowLastScreen]) {
   625 			windowScreen = windowLastScreen;
   626 		} else {
   627 			[windowLastScreen release]; windowLastScreen = nil;
   628 			windowScreen = [NSScreen mainScreen];
   629 		}
   630 	}
   631 	
   632 	NSRect newScreenFrame = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowScreen]] rectValue];
   633 
   634 	if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
   635 		NSRect newWindowFrame = AIRectByAligningRect_edge_toRect_edge_([window frame], [self windowSlidOffScreenEdgeMask],
   636 																	   newScreenFrame, [self windowSlidOffScreenEdgeMask]);
   637 		[[self window] setFrame:newWindowFrame display:NO];
   638 
   639 		[self delayWindowSlidingForInterval:2];
   640 		[self slideWindowOnScreenWithAnimation:NO];
   641 		
   642 	}
   643 
   644 	[contactListController contactListDesiredSizeChanged];
   645 	
   646 	currentScreen = [window screen];
   647 	currentScreenFrame = newScreenFrame;
   648 	[self setSavedFrame:[window frame]];
   649 }
   650 
   651 - (void)screenParametersChanged:(NSNotification *)notification
   652 {
   653 	/* Wait until the next run loop so the class method has definitely updated our screen sliding borders. */
   654 	[self performSelector:@selector(respondToScreenParametersChanged:)
   655 			   withObject:notification
   656 			   afterDelay:0];
   657 }
   658 
   659 // Printing
   660 #pragma mark Printing
   661 - (void)adiumPrint:(id)sender
   662 {
   663 	[contactListView print:sender];
   664 }
   665 
   666 // Dock-like hiding -----------------------------------------------------------------------------------------------------
   667 #pragma mark Dock-like hiding
   668 
   669 + (void)updateScreenSlideBoundaryRect:(id)sender
   670 {
   671 	NSArray *screens = [NSScreen screens];
   672 	NSInteger numScreens = [screens count];
   673 	
   674 	[screenSlideBoundaryRectDictionary release];
   675 	screenSlideBoundaryRectDictionary = [[NSMutableDictionary alloc] initWithCapacity:numScreens];
   676 
   677 	if (numScreens > 0) {
   678 		//The menubar screen is a special case - the menubar is not a part of the rect we're interested in
   679 		NSScreen	*menubarScreen = [screens objectAtIndex:0];
   680 		NSRect		screenSlideBoundaryRect;
   681 
   682 		screenSlideBoundaryRect = [menubarScreen frame];
   683 		screenSlideBoundaryRect.size.height = NSMaxY([menubarScreen visibleFrame]) - NSMinY([menubarScreen frame]);
   684 		[screenSlideBoundaryRectDictionary setObject:[NSValue valueWithRect:screenSlideBoundaryRect]
   685 											  forKey:[NSValue valueWithNonretainedObject:menubarScreen]];
   686 
   687 		for (NSInteger i = 1; i < numScreens; i++) {
   688 			NSScreen *screen = [screens objectAtIndex:i];
   689 			[screenSlideBoundaryRectDictionary setObject:[NSValue valueWithRect:[screen frame]]
   690 												  forKey:[NSValue valueWithNonretainedObject:screen]];
   691 		}
   692 	}
   693 }
   694 
   695 /*!
   696  * @brief Adium unhid
   697  *
   698  * If the contact list is open but not visible when we unhide, we should always display it; it should not, however, steal focus.
   699  */
   700 - (void)applicationDidUnhide:(NSNotification *)notification
   701 {
   702 	if (![[self window] isVisible]) {
   703 		[self showWindowInFrontIfAllowed:NO];
   704 	}
   705 }
   706 
   707 - (BOOL)windowShouldHideOnDeactivate
   708 {
   709 	return (windowHidingStyle == AIContactListWindowHidingStyleBackground);
   710 }
   711 
   712 /*!
   713  * @brief Called on a delay by -[self slideWindowIfNeeded:]
   714  *
   715  * This is a separate function so that the call to it may be canceled if the mouse doesn't
   716  * remain in position long enough.
   717  */
   718 - (void)slideWindowOnScreenAfterDelay
   719 {
   720 	waitingToSlideOnScreen = NO;
   721 
   722 	//If we're hiding the window (generally) but now sliding it on screen, make sure it's on top
   723 	if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
   724 		[self setWindowLevel:NSFloatingWindowLevel];
   725 		
   726 		[[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
   727 		
   728 		overrodeWindowLevel = YES;
   729 	}
   730 	
   731 	[self slideWindowOnScreen];	
   732 }
   733 
   734 /*!
   735  * @brief Check what behavior the window should perform and initiate it
   736  *
   737  * Called regularly by a repeating timer to check mouse position against window position.
   738  */
   739 - (void)slideWindowIfNeeded:(id)sender
   740 {
   741 	if ([self shouldSlideWindowOnScreen]) {
   742 		if (!waitingToSlideOnScreen) {
   743 			[self performSelector:@selector(slideWindowOnScreenAfterDelay)
   744 					   withObject:nil
   745 					   afterDelay:WINDOW_SLIDING_DELAY];
   746 			waitingToSlideOnScreen = YES;
   747 		}
   748 	} else {
   749 		if (waitingToSlideOnScreen) {
   750 			/* If we were waiting to slide on screen but the mouse moved out of position too soon,
   751 			 * cancel the selector which would slide us on screen.
   752 			 */
   753 			waitingToSlideOnScreen = NO;
   754 			[[self class] cancelPreviousPerformRequestsWithTarget:self
   755 														 selector:@selector(slideWindowOnScreenAfterDelay)
   756 														   object:nil];
   757 		}
   758 
   759 		if ([self shouldSlideWindowOffScreen]) {
   760 			AIRectEdgeMask adjacentEdges = [self slidableEdgesAdjacentToWindow];
   761 			
   762 			if (adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask)) {
   763 				[self slideWindowOffScreenEdges:(adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask))];
   764 			} else {
   765 				[self slideWindowOffScreenEdges:adjacentEdges];
   766 			}
   767 			
   768 			/* If we're hiding the window (generally) but now sliding it off screen, set it to kCGBackstopMenuLevel and don't
   769 			 * let it participate in expose.
   770 			 */
   771 			if (overrodeWindowLevel &&
   772 				windowHidingStyle == AIContactListWindowHidingStyleSliding) {
   773 				[self setWindowLevel:kCGBackstopMenuLevel];
   774 				
   775 				[[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
   776 				
   777 				overrodeWindowLevel = YES;
   778 			}
   779 			
   780 		} else if (overrodeWindowLevel &&
   781 				   ([self slidableEdgesAdjacentToWindow] == AINoEdges) &&
   782 				   ([self windowSlidOffScreenEdgeMask] == AINoEdges)) {
   783 			/* If the window level was overridden at some point and now we:
   784 			 *   1. Are on screen AND
   785 			 *   2. No longer have any edges eligible for sliding
   786 			 * we should restore our window level.
   787 			 */
   788 			[self setWindowLevel:levelForAIWindowLevel(windowLevel)];
   789 			
   790 			[[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
   791 			
   792 			overrodeWindowLevel = NO;
   793 		}
   794 	}
   795 }
   796 
   797 - (BOOL)shouldSlideWindowOnScreen
   798 {
   799 	BOOL shouldSlide = NO;
   800 	
   801 	if (([self windowSlidOffScreenEdgeMask] != AINoEdges) &&
   802 		![NSApp isHidden]) {
   803 		if (slideOnlyInBackground && [NSApp isActive]) {
   804 			//We only slide while in the background, and the app is not in the background. Slide on screen.
   805 			shouldSlide = YES;
   806 
   807 		} else if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
   808 			//Slide on screen if the mouse position indicates we should
   809 			shouldSlide = [self shouldSlideWindowOnScreen_mousePositionStrategy];
   810 		} else {
   811 			//It's slid off-screen... and it's not supposed to be sliding at all.  Slide back on screen!
   812 			shouldSlide = YES;
   813 		}
   814 	}
   815 
   816 	return shouldSlide;
   817 }
   818 
   819 - (BOOL)shouldSlideWindowOffScreen
   820 {
   821 	BOOL shouldSlide = NO;
   822 	
   823 	if ((windowHidingStyle == AIContactListWindowHidingStyleSliding) &&
   824 		!preventHiding &&
   825 		([self windowSlidOffScreenEdgeMask] == AINoEdges) &&
   826 		(!(slideOnlyInBackground && [NSApp isActive]))) {
   827 		shouldSlide = [self shouldSlideWindowOffScreen_mousePositionStrategy];
   828 	}
   829 
   830 	return shouldSlide;
   831 }
   832 
   833 // slide off screen if the window is aligned to a screen edge and the mouse is not in the strip of screen 
   834 // you'd get by translating the window along the screen edge.  This is the dock's behavior.
   835 - (BOOL)shouldSlideWindowOffScreen_mousePositionStrategy
   836 {
   837 	BOOL shouldSlideOffScreen = NO;
   838 	
   839 	NSWindow *window = [self window];
   840 	NSRect windowFrame = [window frame];
   841 	NSPoint mouseLocation = [NSEvent mouseLocation];
   842 	
   843 	AIRectEdgeMask slidableEdgesAdjacentToWindow = [self slidableEdgesAdjacentToWindow];
   844 	NSRectEdge screenEdge;
   845 	for (screenEdge = 0; screenEdge < 4; screenEdge++) {		
   846 		if (slidableEdgesAdjacentToWindow & (1 << screenEdge)) {
   847 			CGFloat distanceMouseOutsideWindow = AISignedExteriorDistanceRect_edge_toPoint_(windowFrame, AIOppositeRectEdge_(screenEdge), mouseLocation);
   848 			if (distanceMouseOutsideWindow > WINDOW_SLIDING_MOUSE_DISTANCE_TOLERANCE)
   849 				shouldSlideOffScreen = YES;
   850 		}
   851 	}
   852 	
   853 	/* Don't allow the window to slide off if the user is dragging
   854 	 * This method is hacky and does not completely work.  is there a way to detect if the mouse is down?
   855 	 */
   856 	NSEventType currentEventType = [[NSApp currentEvent] type];
   857 	if (currentEventType == NSLeftMouseDragged ||
   858 		currentEventType == NSRightMouseDragged ||
   859 		currentEventType == NSOtherMouseDragged ||
   860 		currentEventType == NSPeriodic) {
   861 		shouldSlideOffScreen = NO;
   862 	}	
   863 	
   864 	return shouldSlideOffScreen;
   865 }
   866 
   867 // note: may be inaccurate when mouse is up against an edge 
   868 - (NSScreen *)screenForPoint:(NSPoint)point
   869 {
   870 	for (NSScreen *pointScreen in [NSScreen screens]) {
   871 		if (NSPointInRect(point, NSInsetRect([pointScreen frame], -1, -1)))
   872 			return pointScreen;
   873 	}
   874 	return nil;
   875 }	
   876 
   877 - (NSRect)squareRectWithCenter:(NSPoint)point sideLength:(CGFloat)sideLength
   878 {
   879 	return NSMakeRect(point.x - sideLength*0.5f, point.y - sideLength*0.5f, sideLength, sideLength);
   880 }
   881 
   882 - (BOOL)pointIsInScreenCorner:(NSPoint)point
   883 {
   884 	BOOL inCorner = NO;
   885 	NSScreen *menubarScreen = [[NSScreen screens] objectAtIndex:0];
   886 	CGFloat 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)
   887 	
   888 	NSRect screenFrame = [[self screenForPoint:point] frame];
   889 	NSPoint lowerLeft  = screenFrame.origin;
   890 	NSPoint upperRight = NSMakePoint(NSMaxX(screenFrame), NSMaxY(screenFrame));
   891 	NSPoint lowerRight = NSMakePoint(upperRight.x, lowerLeft.y);
   892 	NSPoint upperLeft  = NSMakePoint(lowerLeft.x, upperRight.y);
   893 	
   894 	CGFloat sideLength = menubarHeight * 2.0f;
   895 	inCorner = (NSPointInRect(point, [self squareRectWithCenter:lowerLeft sideLength:sideLength])
   896 				|| NSPointInRect(point, [self squareRectWithCenter:lowerRight sideLength:sideLength])
   897 				|| NSPointInRect(point, [self squareRectWithCenter:upperLeft sideLength:sideLength])
   898 				|| NSPointInRect(point, [self squareRectWithCenter:upperRight sideLength:sideLength]));
   899 	
   900 	return inCorner;
   901 }
   902 
   903 /*!
   904  * @brief Should the window be slid on screen given the mouse's position?
   905  *
   906  * This method will never return YES of the cl is slid into a corner, which shouldn't happen, or if the mouse is in a corner.
   907  *
   908  * @result YES if the mouse is against all edges of the screen where we previously slid the window and not in a corner.
   909  */
   910 - (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy
   911 {
   912 	if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
   913 		NSPoint mouseLocation = [NSEvent mouseLocation];
   914 		//Initially, assume the mouse is not in an appropriate position
   915 		BOOL	mouseNearSlideOffEdges = NO;
   916 
   917 		NSRectEdge	screenEdge;
   918 		NSRect		screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowLastScreen]] rectValue];
   919 		/* Only look at the screen in which the mouse currently resides.
   920 		 * The mouse may be in no screen if it is over the menu bar.
   921 		 */
   922 		if (NSPointInRect(mouseLocation, screenSlideBoundaryRect)) {
   923 			//Check each edge
   924 			for (screenEdge = 0; screenEdge < 4; screenEdge++) {
   925 				//But we only care about an edge off of which the window has slid
   926 				if (windowSlidOffScreenEdgeMask & (1 << screenEdge)) {
   927 					CGFloat mouseOutsideSlideBoundaryRectDistance = AISignedExteriorDistanceRect_edge_toPoint_(screenSlideBoundaryRect,
   928 																											   screenEdge,
   929 																											   mouseLocation);
   930 					//The mouse must be within MOUSE_EDGE_SLIDE_ON_DISTANCE of every slid-off edge to bring the window back on-screen
   931 					if(mouseOutsideSlideBoundaryRectDistance < -MOUSE_EDGE_SLIDE_ON_DISTANCE) {
   932 						mouseNearSlideOffEdges = NO;
   933 						break;
   934 					} else {
   935 						mouseNearSlideOffEdges = YES;							
   936 					}
   937 				}
   938 			}
   939 		}
   940 
   941 		return mouseNearSlideOffEdges && ![self pointIsInScreenCorner:mouseLocation];
   942 
   943 	} else {
   944 		return NO;
   945 	}
   946 }
   947 
   948 #pragma mark Dock-like hiding
   949 
   950 - (NSScreen *)windowLastScreen
   951 {
   952 	return windowLastScreen;
   953 }
   954 
   955 - (BOOL)animationShouldStart:(NSAnimation *)animation
   956 {
   957 	if(![animation isEqual:windowAnimation])
   958 		return YES;
   959 	
   960 	//Whenever an animation starts, we should be using the normal shadow setting
   961 	[[self window] setHasShadow:listHasShadow];
   962 	
   963 	//Don't let docking interfere with the animation
   964 	if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
   965 		[(id)[self window] setDockingEnabled:NO];
   966 	
   967 	if (windowSlidOffScreenEdgeMask == AINoEdges) {
   968 		[[self window] setAlphaValue:previousAlpha];
   969 		AILogWithSignature(@"Set window to previous alpha of %f", previousAlpha);
   970 	}
   971 
   972 	return YES;
   973 }
   974 
   975 - (void)animationDidEnd:(NSAnimation*)animation
   976 {
   977 	if([animation isEqual:windowAnimation]) {
   978 		//Restore docking behavior	
   979 		if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
   980 			[(id)[self window] setDockingEnabled:YES];
   981 		
   982 		if (windowSlidOffScreenEdgeMask == AINoEdges) {
   983 			//When the window is offscreen, its horizontal autosizing can't occur. Size it now.
   984 			[contactListController contactListDesiredSizeChanged];
   985 
   986 		} else {
   987 			//Offscreen windows should be told not to cast a shadow
   988 			[[self window] setHasShadow:NO];	
   989 
   990 			previousAlpha = [[self window] alphaValue];
   991 			[[self window] setAlphaValue:0.0];
   992 			AILogWithSignature(@"Previous alpha is now %f; window set to alpha 0.0 ", previousAlpha);
   993 		}
   994 		
   995 		self.windowAnimation = nil;
   996 	}
   997 	
   998 	if (animation == filterBarAnimation) {
   999 		if (filterBarIsVisible) {
  1000 			// If the filter bar is already visible, remove it from its superview.
  1001 			[filterBarView removeFromSuperview];
  1002 			
  1003 			// Set the first responder back to the contact list view.
  1004 			[[self window] makeFirstResponder:contactListView];
  1005 			
  1006 			[contactListView selectItemsInArray:filterBarPreviouslySelected];
  1007 			
  1008 			// Since this wasn't a user-initiated selection change, we need to post a notification for it.
  1009 			[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactSelectionChanged
  1010 																object:nil];
  1011 			
  1012 			[filterBarPreviouslySelected release]; filterBarPreviouslySelected = nil;
  1013 			
  1014 			filterBarIsVisible = NO;
  1015 		} else {
  1016 			// If the filter bar wasn't visible, make it the first responder.
  1017 			[[self window] makeFirstResponder:searchField]; 
  1018 			
  1019 			// Set the filter bar as the next responder so the chain works for things like the info inspector
  1020 			[filterBarView setNextResponder:contactListView];
  1021 			
  1022 			// Bring the contact list to front, in case the find command was triggered from another window like the info inspector
  1023 			[[self window] makeKeyAndOrderFront:nil];
  1024 			
  1025 			filterBarPreviouslySelected = [[contactListView arrayOfSelectedItems] retain];
  1026 			
  1027 			filterBarIsVisible = YES;
  1028 		}
  1029 		
  1030 		// Let the contact list controller know that our size has changed.
  1031 		[contactListController contactListDesiredSizeChanged];
  1032 		
  1033 		// We're no longer animating.
  1034 		self.filterBarAnimation = nil;
  1035 	}
  1036 }
  1037 
  1038 - (BOOL)keepListOnScreenWhenSliding
  1039 {
  1040 	return NO;
  1041 }
  1042 
  1043 /*!
  1044  * @brief Slide the window to a given point
  1045  *
  1046  * windowSlidOffScreenEdgeMask must already be set to the resulting offscreen mask (or 0 if the window is sliding on screen)
  1047  *
  1048  * A standard window (titlebar window) will crash if told to setFrame completely offscreen. Also, using our own movement we can more precisely
  1049  * control the movement speed and acceleration.
  1050  */
  1051 - (void)slideWindowToPoint:(NSPoint)targetPoint
  1052 {	
  1053 	NSWindow				*myWindow = [self window];
  1054 	NSScreen				*windowScreen;
  1055 
  1056 	windowScreen = [myWindow screen];
  1057 	if (!windowScreen) windowScreen = [self windowLastScreen];
  1058 	if (!windowScreen) windowScreen = [NSScreen mainScreen];
  1059 	
  1060 	NSRect	frame = [myWindow frame];
  1061 	CGFloat yOff = (targetPoint.y + NSHeight(frame)) - NSMaxY([windowScreen frame]);
  1062 	if (windowScreen == [[NSScreen screens] objectAtIndex:0]) yOff -= [[NSApp mainMenu] menuBarHeight];
  1063 	if (yOff > 0) targetPoint.y -= yOff;
  1064 	
  1065 	frame.origin = targetPoint;
  1066 	
  1067 	if ((windowSlidOffScreenEdgeMask != AINoEdges) &&
  1068 		[self keepListOnScreenWhenSliding]) {
  1069 		switch (windowSlidOffScreenEdgeMask) {
  1070 			case AIMinXEdgeMask:
  1071 				frame.origin.x += 1;
  1072 				break;
  1073 			case AIMaxXEdgeMask:
  1074 				frame.origin.x -= 1;
  1075 				break;
  1076 			case AIMaxYEdgeMask:
  1077 				frame.origin.y -= 1;
  1078 				break;
  1079 			case AIMinYEdgeMask:
  1080 				frame.origin.y += 1;
  1081 				break;
  1082 			case AINoEdges:
  1083 				//We'll never get here
  1084 				break;
  1085 		}
  1086 	}
  1087 	
  1088 	if (windowAnimation) {
  1089 		[windowAnimation stopAnimation];
  1090 		self.windowAnimation = nil;
  1091 	}
  1092 
  1093 	self.windowAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:
  1094 							 [NSArray arrayWithObject:
  1095 							  [NSDictionary dictionaryWithObjectsAndKeys:
  1096 							   myWindow, NSViewAnimationTargetKey,
  1097 							   [NSValue valueWithRect:frame], NSViewAnimationEndFrameKey,
  1098 							   nil]]] autorelease];
  1099 	[windowAnimation setFrameRate:0.0];
  1100 	[windowAnimation setDuration:0.25];
  1101 	[windowAnimation setDelegate:self];
  1102 	[windowAnimation setAnimationBlockingMode:NSAnimationNonblocking];
  1103 	[windowAnimation startAnimation];
  1104 }
  1105 
  1106 - (void)moveWindowToPoint:(NSPoint)inOrigin
  1107 {
  1108 	[[self window] setFrameOrigin:inOrigin];
  1109 
  1110 	if (windowSlidOffScreenEdgeMask == AINoEdges) {
  1111 		/* When the window is offscreen, there are no constraints on its size, for example it will grow downwards as much as
  1112 		* it needs to to accomodate new rows.  Now that it's onscreen, there are constraints.
  1113 		*/
  1114 		[contactListController contactListDesiredSizeChanged];
  1115 		[[self window] setAlphaValue:previousAlpha];
  1116 		AILogWithSignature(@"Set window to previous alpha of %f", previousAlpha);
  1117 	}
  1118 }
  1119 
  1120 static BOOL AIScreenRectEdgeAdjacentToAnyOtherScreen(NSRectEdge edge, NSScreen *screen)
  1121 {
  1122 	NSArray  *screens = [NSScreen screens];
  1123 	NSUInteger numScreens = [screens count];
  1124 	if (numScreens > 1) {
  1125 		NSRect	screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:screen]] rectValue];
  1126 		NSRect	shiftedScreenFrame = screenSlideBoundaryRect;
  1127 		BOOL	isAdjacent = NO;
  1128 		
  1129 		switch(edge) {
  1130 			case NSMinXEdge:
  1131 				shiftedScreenFrame.origin.x -= 1;
  1132 				break;
  1133 			case NSMinYEdge:
  1134 				shiftedScreenFrame.origin.y -= 1;
  1135 				break;
  1136 			case NSMaxXEdge:
  1137 				shiftedScreenFrame.size.width += 1;
  1138 				break;
  1139 			case NSMaxYEdge:
  1140 				shiftedScreenFrame.size.height += 1;
  1141 				break;
  1142 		}
  1143 
  1144 		for (NSInteger i = 0; i < numScreens; i++) {
  1145 			NSScreen *otherScreen = [screens objectAtIndex:i];
  1146 			if (otherScreen != screen) {
  1147 				if (NSIntersectsRect([otherScreen frame], shiftedScreenFrame)) {
  1148 					isAdjacent = YES;
  1149 					break;
  1150 				}
  1151 			}	
  1152 		}
  1153 
  1154 		return isAdjacent;
  1155 		
  1156 	} else {
  1157 		return NO;
  1158 	}
  1159 }
  1160 
  1161 /*!
  1162  * @brief Find the mask specifying what edges are potentially slidable for our window
  1163  *
  1164  * @result AIRectEdgeMask, which is 0 if no edges are slidable
  1165  */
  1166 - (AIRectEdgeMask)slidableEdgesAdjacentToWindow
  1167 {
  1168 	AIRectEdgeMask slidableEdges = 0;
  1169 
  1170 	NSWindow *window = [self window];
  1171 	NSRect	 windowFrame = [window frame];	
  1172 	NSScreen *windowScreen = [window screen];
  1173 	NSRect	 screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowScreen]] rectValue];
  1174 
  1175 	NSRectEdge edge;
  1176 	for (edge = 0; edge < 4; edge++) {
  1177 		if ((SLIDE_ALLOWED_RECT_EDGE_MASK & (1 << edge)) &&
  1178 			(AIRectIsAligned_edge_toRect_edge_tolerance_(windowFrame,
  1179 														 edge,
  1180 														 screenSlideBoundaryRect,
  1181 														 edge,
  1182 														 WINDOW_ALIGNMENT_TOLERANCE)) &&
  1183 			(!AIScreenRectEdgeAdjacentToAnyOtherScreen(edge, windowScreen))) { 
  1184 			slidableEdges |= (1 << edge);
  1185 		}
  1186 	}
  1187 	
  1188 	return slidableEdges;
  1189 }
  1190 
  1191 - (void)slideWindowOffScreenEdges:(AIRectEdgeMask)rectEdgeMask
  1192 {
  1193 	NSWindow	*window;
  1194 	NSRect		newWindowFrame;
  1195 	NSRectEdge	edge;
  1196 
  1197 	if (rectEdgeMask == AINoEdges)
  1198 		return;
  1199 
  1200 	window = [self window];
  1201 	newWindowFrame = [window frame];
  1202 
  1203 	[self setSavedFrame:newWindowFrame];
  1204 
  1205 	[windowLastScreen release];
  1206 	windowLastScreen = [[window screen] retain];
  1207 
  1208 	NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowLastScreen]] rectValue];
  1209 
  1210 	for (edge = 0; edge < 4; edge++) {
  1211 		if (rectEdgeMask & (1 << edge)) {
  1212 			newWindowFrame = AIRectByAligningRect_edge_toRect_edge_(newWindowFrame,
  1213 																	AIOppositeRectEdge_(edge),
  1214 																	screenSlideBoundaryRect,
  1215 																	edge);
  1216 		}
  1217 	}
  1218 
  1219 	windowSlidOffScreenEdgeMask |= rectEdgeMask;
  1220 
  1221 	[self slideWindowToPoint:newWindowFrame.origin];
  1222 }
  1223 
  1224 - (void)slideWindowOnScreenWithAnimation:(BOOL)animate
  1225 {
  1226 	if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
  1227 		NSWindow	*window = [self window];
  1228 		NSRect		windowFrame = [window frame];
  1229 		
  1230 		if (!NSEqualRects(windowFrame, oldFrame)) {
  1231 			//Restore shadow and frame if we're appearing from having slid off-screen
  1232 			[window setHasShadow:[[adium.preferenceController preferenceForKey:KEY_CL_WINDOW_HAS_SHADOW
  1233 																		   group:PREF_GROUP_CONTACT_LIST] boolValue]];			
  1234 			[window orderFront:nil]; 
  1235 			[contactListController contactListWillSlideOnScreen];
  1236 
  1237 			windowSlidOffScreenEdgeMask = AINoEdges;
  1238 
  1239 			if (animate) {
  1240 				[self slideWindowToPoint:oldFrame.origin];
  1241 			} else {
  1242 				[self moveWindowToPoint:oldFrame.origin];
  1243 			}
  1244 			
  1245 			[windowLastScreen release];	windowLastScreen = nil;
  1246 		}
  1247 	}
  1248 }
  1249 
  1250 - (void)slideWindowOnScreen
  1251 {
  1252 	[self slideWindowOnScreenWithAnimation:YES];
  1253 }
  1254 
  1255 - (void)setPreventHiding:(BOOL)newPreventHiding {
  1256 	preventHiding = newPreventHiding;
  1257 }
  1258 
  1259 - (void)endWindowSlidingDelay
  1260 {
  1261 	[self setPreventHiding:NO];
  1262 }
  1263 
  1264 - (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime
  1265 {
  1266 	[self setPreventHiding:YES];
  1267 	
  1268 	[NSObject cancelPreviousPerformRequestsWithTarget:self
  1269 											 selector:@selector(endWindowSlidingDelay)
  1270 											   object:nil];
  1271 	[self performSelector:@selector(endWindowSlidingDelay)
  1272 			   withObject:nil
  1273 			   afterDelay:inDelayTime];
  1274 }
  1275 
  1276 - (AIRectEdgeMask)windowSlidOffScreenEdgeMask
  1277 {
  1278 	return windowSlidOffScreenEdgeMask;
  1279 }
  1280 
  1281 // Snap Groups Together------------------------------------------------------------------------------------------------
  1282 #pragma mark Snap Groups Together
  1283 
  1284 /*!
  1285  * @brief If window did move and is not docked then snap it to other windows
  1286  */
  1287 - (void)windowDidMove:(NSNotification *)notification
  1288 {
  1289 	BOOL suppressSnapping = [NSEvent shiftKey];
  1290 
  1291 	attachToBottom = nil;
  1292 	
  1293 	if (windowSlidOffScreenEdgeMask == AINoEdges && !suppressSnapping)
  1294 		[self snapToOtherWindows];
  1295 }
  1296 
  1297 /*!
  1298  * @brief Captures mouse up event to check that if the window snapped underneath
  1299  * another window they are merged together
  1300  */
  1301 - (void)mouseUp:(NSEvent *)event {
  1302 	if (attachToBottom) {
  1303 		AIContactList *from = (AIContactList *)[self contactList];
  1304 		AIContactList *to = (AIContactList *)[attachToBottom contactList];
  1305 		
  1306 		for (AIListGroup *group in from) {
  1307 			[adium.contactController moveGroup:group fromContactList:from toContactList:to];
  1308 		}
  1309 		
  1310 		[[NSNotificationCenter defaultCenter] postNotificationName:DetachedContactListIsEmpty
  1311 												  object:from
  1312 												userInfo:nil];
  1313 		[[NSNotificationCenter defaultCenter] postNotificationName:@"Contact_ListChanged"
  1314 												  object:to
  1315 												userInfo:nil]; 
  1316 	}
  1317 	
  1318 	[super mouseUp:event];
  1319 }
  1320 
  1321 /*!
  1322  * @brief Snaps window to windows next to it
  1323  */
  1324 - (void)snapToOtherWindows
  1325 {	
  1326 	NSWindow *myWindow = [self window];
  1327 	NSArray *windows = [[NSApplication sharedApplication] windows];
  1328 	
  1329 	NSWindow *window;
  1330 	
  1331 	NSRect currentFrame = [myWindow frame];
  1332 	NSPoint suggested = currentFrame.origin;
  1333 	
  1334 	// Check to snap to each guide
  1335 	for (window in windows) {
  1336 		// No snapping to itself and it must be within a snapping distance to other windows
  1337 		if ((window != myWindow) &&
  1338 			[window delegate] && [window isVisible] && 
  1339 			[[window delegate] conformsToProtocol:@protocol(AIInterfaceContainer)]) {
  1340 			/* Note: [window delegate] may be invalid if the window is in the middle of closing.
  1341 			 * Checking if it's visible should hopefully cover that case.
  1342 			 */
  1343 			suggested = [self snapTo:window with:currentFrame saveTo:suggested];
  1344 		}
  1345 	}
  1346 
  1347 	[[self window] setFrameOrigin:suggested];
  1348 }
  1349 
  1350 
  1351 /*!
  1352  * @brief Check that window is inside snappable region of other window
  1353  */
  1354 static BOOL isInRangeOfRect(NSRect sourceRect, NSRect targetRect)
  1355 {
  1356 	return NSIntersectsRect(NSInsetRect(sourceRect, -SNAP_DISTANCE, -SNAP_DISTANCE), targetRect);
  1357 }
  1358 
  1359 /*!
  1360  * @brief Check if points are close enough to be snapped together
  1361  */
  1362 static BOOL canSnap(CGFloat a, CGFloat b)
  1363 {
  1364 	return (abs(a - b) <= SNAP_DISTANCE);
  1365 }
  1366 
  1367 - (NSPoint)snapTo:(NSWindow*)neighborWindow with:(NSRect)currentRect saveTo:(NSPoint)location{
  1368 	NSRect neighbor = [neighborWindow frame];
  1369 	NSPoint spacing = [self windowSpacing];
  1370 	NSUInteger overlap = 0;
  1371 	NSUInteger bottom = 0;
  1372 	
  1373 	if (!NSEqualRects(neighbor,currentRect) && isInRangeOfRect(currentRect, neighbor)) {
  1374 		// X Snapping
  1375 		if (canSnap(NSMaxX(currentRect), NSMinX(neighbor))) {
  1376 			location.x = NSMinX(neighbor) - NSWidth(currentRect) - spacing.x;
  1377 		} else if (canSnap(NSMinX(currentRect), NSMaxX(neighbor))) {
  1378 			location.x = NSMaxX(neighbor) + spacing.x;
  1379 		} else if (canSnap(NSMinX(currentRect), NSMinX(neighbor))) {
  1380 			location.x = NSMinX(neighbor);
  1381 			overlap++;
  1382 			bottom++;
  1383 		}
  1384 		
  1385 		// Y Snapping
  1386 		if (canSnap(NSMaxY(neighbor), NSMaxY(currentRect))) {
  1387 			location.y = NSMaxY(neighbor) - NSHeight(currentRect);
  1388 			overlap++;
  1389 		} else if (canSnap(NSMinY(neighbor), NSMaxY(currentRect))) {
  1390 			location.y = NSMinY(neighbor) - NSHeight(currentRect) - spacing.y;
  1391 			bottom++;
  1392 		} else if (canSnap(NSMaxY(neighbor), NSMinY(currentRect))) {
  1393 			location.y = NSMaxY(neighbor) + spacing.y;
  1394 		} else if (canSnap(NSMinY(neighbor), NSMinY(currentRect))) {
  1395 			location.y = NSMinY(neighbor);
  1396 			overlap++;
  1397 		}
  1398 		
  1399 	}	
  1400 	
  1401 	// If we snapped on top of neighbor
  1402 	if (overlap == 2)
  1403 		return currentRect.origin;
  1404 
  1405 	// Save window that we could possible attach to
  1406 	if (bottom == 2)
  1407 		attachToBottom = [neighborWindow delegate];
  1408 	
  1409 	return location;
  1410 }
  1411 
  1412 
  1413 /*!
  1414  * @brief Gets space that windows should be apart by based on current window style
  1415  */
  1416 - (NSPoint)windowSpacing {
  1417 	AIContactListWindowStyle style = [[adium.preferenceController preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE
  1418 														  group:PREF_GROUP_APPEARANCE] integerValue];
  1419 	NSInteger space = [[adium.preferenceController preferenceForKey:@"Group Top Spacing" 
  1420 														  group:@"List Layout"] integerValue];
  1421 	
  1422 	switch (style) {
  1423 		case AIContactListWindowStyleStandard:
  1424 		case AIContactListWindowStyleBorderless:
  1425 		case AIContactListWindowStyleGroupChat:
  1426 			return NSMakePoint(0,0);
  1427 		case AIContactListWindowStyleGroupBubbles:
  1428 		case AIContactListWindowStyleContactBubbles:
  1429 		case AIContactListWindowStyleContactBubbles_Fitted:
  1430 			return NSMakePoint(space,space-WINDOW_ALIGNMENT_TOLERANCE);
  1431 	}
  1432 	return NSMakePoint(0,0);
  1433 }
  1434 
  1435 #pragma mark Filtering
  1436 /*!
  1437  * @brief Toggles the find bar on, or brings it into focus if it is already visible
  1438  */
  1439 - (void)toggleFindPanel:(id)sender;
  1440 {
  1441 	if (filterBarIsVisible) {
  1442 		[[self window] makeFirstResponder:searchField]; 
  1443 		
  1444 	} else if ([contactListView numberOfRows] > 0) {
  1445 		filterBarShownAutomatically = NO;
  1446 		[self showFilterBarWithAnimation:YES];
  1447 		
  1448 	} else {
  1449 		NSBeep();
  1450 	}
  1451 }
  1452 
  1453 /*!
  1454  * @brief Hide the filter bar
  1455  */
  1456 - (IBAction)hideFilterBar:(id)sender;
  1457 {
  1458 	[self hideFilterBarWithAnimation:YES];
  1459 }
  1460 
  1461 /*!
  1462  * @brief Show the filter bar
  1463  *
  1464  * @param useAnimation If YES, the filter bar will scroll into view, otherwise it appears immediately
  1465  */
  1466 - (void)showFilterBarWithAnimation:(BOOL)useAnimation
  1467 {
  1468 	if (filterBarIsVisible || filterBarAnimation)
  1469 		return;
  1470 	
  1471 	// While the filter bar is shown, temporarily disable automatic horizontal resizing
  1472 	contactListController.autoResizeHorizontally = NO;
  1473 	
  1474 	// Disable contact list animation while the filter bar is shown
  1475 	[contactListView setEnableAnimation:NO];
  1476 	
  1477 	// Animate the filter bar into view	
  1478 	[self animateFilterBarWithDuration:(useAnimation ? 0.15f : 0.0)];
  1479 }
  1480 
  1481 /*!
  1482  * @brief Hide the filter bar
  1483  *
  1484  * @param useAnimation If YES, the filter bar will scroll out of view, otherwise it disappears immediately
  1485  */
  1486 - (void)hideFilterBarWithAnimation:(BOOL)useAnimation
  1487 {
  1488 	if (!filterBarIsVisible || filterBarAnimation)
  1489 		return;
  1490 	
  1491 	// Clear the search field so that visibility is reset
  1492 	[searchField setStringValue:@""];
  1493 	[self filterContacts:searchField];
  1494 	
  1495 	// Restore the default settings which we temporarily disabled previously
  1496 	contactListController.autoResizeHorizontally = [[adium.preferenceController preferenceForKey:KEY_LIST_LAYOUT_HORIZONTAL_AUTOSIZE group:PREF_GROUP_APPEARANCE] boolValue];
  1497 	
  1498 	[contactListView setEnableAnimation:[[adium.preferenceController preferenceForKey:KEY_CL_ANIMATE_CHANGES
  1499 										  group:PREF_GROUP_CONTACT_LIST] boolValue]];
  1500 	
  1501 	// Animate the filter bar out of view
  1502 	[self animateFilterBarWithDuration:(useAnimation ? 0.15f : 0.0)];
  1503 }
  1504 
  1505 /*!
  1506  * @brief Animates the filter bar in and out of view
  1507  *
  1508  * @param duration The duration the animation will last
  1509  */
  1510 - (void)animateFilterBarWithDuration:(CGFloat)duration
  1511 {
  1512 	NSView *targetView = ([contactListView enclosingScrollView] ? (NSView *)[contactListView enclosingScrollView] : contactListView);
  1513 	NSRect targetFrame = [targetView frame];
  1514 	NSDictionary *targetViewDict, *filterBarDict;
  1515 	
  1516 	// Contact list resizing
  1517 	if (filterBarIsVisible) {
  1518 		targetFrame.size.height = NSHeight(targetFrame) + NSHeight([filterBarView bounds]);
  1519 		
  1520 	} else {
  1521 		/* We can only have a height less than the filter bar view if we are autosizing vertically, as
  1522 		 * there is a minimum height otherwise which is larger.  We can therefore increase our window size to allow space
  1523 		 * for the filter bar with impunity and without undoing this when hiding the bar, as the autosizing of the contact
  1524 		 * list will get us back to the right size later.
  1525 		 */
  1526 		if (NSHeight(targetFrame) < (NSHeight([filterBarView bounds]) * 2)) {
  1527 			NSRect windowFrame = [[targetView window] frame];
  1528 			
  1529 			[[targetView window] setFrame:NSMakeRect(NSMinX(windowFrame), NSMinY(windowFrame) - NSHeight([filterBarView bounds]),
  1530 													 NSWidth(windowFrame), NSHeight(windowFrame) + NSHeight([filterBarView bounds]))
  1531 								  display:NO
  1532 								  animate:NO];
  1533 			
  1534 			targetFrame = [targetView frame];
  1535 		}
  1536 		
  1537 		targetFrame.size.height = NSHeight(targetFrame) - NSHeight([filterBarView bounds]);
  1538 	}
  1539 	
  1540 	/* Setting a frame's height to 0 can permanently destroy its ability to display properly.
  1541 	 * This is the case with an NSOutlineView. If our contact list was invisibile (because no contacts
  1542 	 * were visible), create a 1 pixel border rather than traumatizing it for life.
  1543 	 */
  1544 	if (targetFrame.size.height == 0)
  1545 		targetFrame.size.height = 1;
  1546     
  1547     // Filter bar resizing
  1548     NSRect barTargetFrame = contactListView.enclosingScrollView.frame;
  1549     if (filterBarIsVisible) {
  1550         barTargetFrame.size.height = NSHeight(barTargetFrame) + NSHeight(filterBarView.bounds);
  1551     } else {
  1552         barTargetFrame.size.height = NSHeight(barTargetFrame) - NSHeight(filterBarView.bounds);
  1553     }
  1554 	
  1555 	if (!filterBarIsVisible) {
  1556 		// If the filter bar isn't already visible        
  1557 		[filterBarView setFrame:NSMakeRect(NSMinX(barTargetFrame),
  1558 										   NSHeight([contactListView frame]),
  1559 										   NSWidth(barTargetFrame),
  1560 										   NSHeight([filterBarView bounds]))];
  1561         
  1562 		// Attach the filter bar to the window
  1563 		[[[self window] contentView] addSubview:filterBarView];
  1564 	}
  1565 	
  1566 	filterBarDict = [NSDictionary dictionaryWithObjectsAndKeys:filterBarView, NSViewAnimationTargetKey,
  1567 					 [NSValue valueWithRect:NSMakeRect(NSMinX(barTargetFrame), NSHeight(barTargetFrame),
  1568 													   NSWidth(barTargetFrame), NSHeight([filterBarView bounds]))], NSViewAnimationEndFrameKey, nil];
  1569 	
  1570 	targetViewDict = [NSDictionary dictionaryWithObjectsAndKeys:targetView, NSViewAnimationTargetKey,
  1571 					  [NSValue valueWithRect:targetFrame], NSViewAnimationEndFrameKey, nil];
  1572 	
  1573 	self.filterBarAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:
  1574 																				targetViewDict,
  1575 																				filterBarDict,
  1576 																				nil]] autorelease];
  1577 	[filterBarAnimation setDuration:duration];
  1578 	[filterBarAnimation setAnimationBlockingMode:NSAnimationBlocking];
  1579 	[filterBarAnimation setDelegate:self];
  1580 	
  1581 	// Start the animation
  1582 	[filterBarAnimation startAnimation];
  1583 }
  1584 
  1585 /*!
  1586  * @brief Called when the window loses focus
  1587  */
  1588 - (void)windowDidResignMain:(NSNotification *)sender
  1589 {
  1590 	/* If the filter bar was shown by type-to-find (but not by command-F), and the window is no longer main,
  1591 	 * assume the user is done and hide the filter bar.
  1592 	 */
  1593 	if (filterBarIsVisible && filterBarShownAutomatically)
  1594 		[self hideFilterBarWithAnimation:NO];
  1595 }
  1596 
  1597 /*!
  1598  * @brief Forward typing events from the contact list to the filter bar
  1599  */
  1600 - (BOOL)forwardKeyEventToFindPanel:(NSEvent *)theEvent;
  1601 {
  1602 	//if we were not searching something before, we need to show the filter bar first without animation
  1603 	NSString	*charString = [theEvent charactersIgnoringModifiers];
  1604 	unichar		pressedChar = 0;
  1605 	
  1606 	//Get the pressed character
  1607 	if ([charString length] == 1) pressedChar = [charString characterAtIndex:0];
  1608 	
  1609 #define NSEscapeFunctionKey 27
  1610 	/* Hitting escape once should clear any existing selection. Keys with functional modifiers pressed should not be passed.
  1611 	 * Home and End should be passed to the find panel only  if it is already visible.
  1612 	 */
  1613 	if (((pressedChar == NSEscapeFunctionKey) && ([contactListView selectedRow] != -1 || !filterBarIsVisible)) ||
  1614 		(([theEvent modifierFlags] & NSCommandKeyMask) || ([theEvent modifierFlags] & NSAlternateKeyMask) || ([theEvent modifierFlags] & NSControlKeyMask)) ||
  1615 		((pressedChar == NSPageUpFunctionKey) || (pressedChar == NSPageDownFunctionKey) || (pressedChar == NSMenuFunctionKey)) ||
  1616 		(!filterBarIsVisible && ((pressedChar == NSHomeFunctionKey) || (pressedChar == NSEndFunctionKey)))) {
  1617 		return NO;
  1618 		
  1619 	} else {
  1620 		if (!filterBarIsVisible) {
  1621 			/* Typing caused the filter bar ot be shown automatically */
  1622 			filterBarShownAutomatically = YES;
  1623 			[self showFilterBarWithAnimation:NO];
  1624 		}
  1625 		
  1626 		[[self window] makeFirstResponder:searchField];
  1627 		[[[self window] fieldEditor:YES forObject:searchField] keyDown:theEvent];
  1628 		
  1629 		return YES;
  1630 	}
  1631 }
  1632 
  1633 /*!
  1634  * @brief Process text commands while on the search field
  1635  */
  1636 - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
  1637 {
  1638 	// Only process commands when we're in the search field.
  1639 	if (control != searchField)
  1640 		return NO;
  1641 	
  1642 	if (command == @selector(insertNewline:)) {
  1643 		// If we have a search term, open a chat with the first contact
  1644 		if (![[textView string] isEqualToString:@""])
  1645 			[self performDefaultActionOnSelectedObject:[contactListView firstVisibleListContact]
  1646 												sender:contactListView];
  1647 		// Hide the filter bar
  1648 		[self hideFilterBarWithAnimation:YES];		
  1649 	} else if(command == @selector(moveDown:)) {
  1650 		// The down arrow functions to move into the contact list view
  1651 		[[self window] makeFirstResponder:contactListView];		
  1652 	} else if(command == @selector(cancelOperation:)) {
  1653 		// Escape hides the filter bar.
  1654 		[self hideFilterBarWithAnimation:YES];
  1655 	} else {
  1656 		// If we didn't process a command, return NO.
  1657 		return NO;
  1658 	}
  1659 	
  1660 	// We processed a command, return YES.
  1661 	return YES;
  1662 }
  1663 
  1664 /*!
  1665  * @brief Filter contacts from the search field
  1666  *
  1667  * This method will expand or contract groups as necessary, as well as handle forwarding the search term to
  1668  * the contact hiding controller.
  1669  */
  1670 - (IBAction)filterContacts:(id)sender;
  1671 {
  1672 	if (![sender isKindOfClass:[NSSearchField class]])
  1673 		return;
  1674 	
  1675 	if (!filterBarExpandedGroups && ![[sender stringValue] isEqualToString:@""]) {
  1676         BOOL modified = NO;
  1677         for (AIListObject *listObject in [self.contactList containedObjects]) {
  1678             if ([listObject isKindOfClass:[AIListGroup class]] && [(AIListGroup *)listObject isExpanded] == NO) {
  1679                 [listObject setValue:[NSNumber numberWithBool:YES] forProperty:@"ExpandedByFiltering" notify:NotifyNever];
  1680                 modified = YES;
  1681             }
  1682         }
  1683         
  1684         filterBarExpandedGroups = YES;
  1685         
  1686         if (modified) {
  1687             [contactListView reloadData];
  1688         }
  1689     } else if (filterBarExpandedGroups && [[sender stringValue] isEqualToString:@""]) {
  1690         BOOL modified = NO;
  1691         for (AIListObject *listObject in [self.contactList containedObjects]) {
  1692             if ([listObject isKindOfClass:[AIListGroup class]] && [listObject boolValueForProperty:@"ExpandedByFiltering"]) {
  1693                 [listObject setValue:[NSNumber numberWithBool:NO] forProperty:@"ExpandedByFiltering" notify:NotifyNever];
  1694                 modified = YES;
  1695             }
  1696         }
  1697         
  1698         filterBarExpandedGroups = NO;
  1699         
  1700         if (modified) {
  1701             [contactListView reloadData];
  1702         }
  1703     }
  1704 	
  1705 	if ([[AIContactHidingController sharedController] filterContacts:[sender stringValue]]) {
  1706 		// Select the first contact; we're guaranteed at least one visible contact.
  1707 		[contactListView selectRowIndexes:[NSIndexSet indexSetWithIndex:[contactListView indexOfFirstVisibleListContact]]
  1708 					 byExtendingSelection:NO];
  1709 		
  1710 		// Since this wasn't a user-initiated selection change, we need to post a notification for it.
  1711 		[[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactSelectionChanged
  1712 															object:nil];
  1713 		
  1714 		[[searchField cell] setTextColor:nil backgroundColor:nil];
  1715 		
  1716 	} else {
  1717 		//White on light red (like Firefox!)
  1718 		[[searchField cell] setTextColor:[NSColor whiteColor] backgroundColor:[NSColor colorWithCalibratedHue:0.983
  1719 																								   saturation:0.43
  1720 																								   brightness:0.99
  1721 																										alpha:1.0]];
  1722 	}
  1723 }
  1724 
  1725 /*!
  1726  * @brief Delegate method for the search field's close button
  1727  */
  1728 - (void)rolloverButton:(AIRolloverButton *)inButton mouseChangedToInsideButton:(BOOL)isInside
  1729 {
  1730 	[button_cancelFilterBar setImage:[NSImage imageNamed:(isInside ? @"FTProgressStopRollover" : @"FTProgressStop")
  1731 												forClass:[self class]]];
  1732 }
  1733 
  1734 @end