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
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIListWindowController.h"
19 #import "AISCLViewPlugin.h"
20 #import <Adium/AIListOutlineView.h>
21 #import <Adium/AIChatControllerProtocol.h>
22 #import <Adium/AIAccountControllerProtocol.h>
23 #import <Adium/AIInterfaceControllerProtocol.h>
24 #import <Adium/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>
42 #import "AISearchFieldCell.h"
44 #define KEY_HIDE_CONTACT_LIST_GROUPS @"Hide Contact List Groups"
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 */
53 #define SNAP_DISTANCE 15.0 /* Distance beween one window's edge and another's at which they should snap together */
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;
64 - (void)showFilterBarWithAnimation:(BOOL)flag;
65 - (void)hideFilterBarWithAnimation:(BOOL)flag;
66 - (void)animateFilterBarWithDuration:(CGFloat)duration;
69 @implementation AIListWindowController
71 @synthesize windowAnimation, filterBarAnimation;
73 static NSMutableDictionary *screenSlideBoundaryRectDictionary = nil;
77 if ([self isEqual:[AIListWindowController class]]) {
78 [[NSNotificationCenter defaultCenter] addObserver:self
79 selector:@selector(updateScreenSlideBoundaryRect:)
80 name:NSApplicationDidChangeScreenParametersNotification
83 [self updateScreenSlideBoundaryRect:nil];
87 + (AIListWindowController *)listWindowControllerForContactList:(AIListObject<AIContainingObject> *)contactList
89 return [[[self alloc] initWithContactList:contactList] autorelease];
92 - (id)initWithContactList:(AIListObject<AIContainingObject> *)contactList
94 if ((self = [self initWithWindowNibName:[[self class] nibName]])) {
98 [NSBundle loadNibNamed:@"Filter Bar" owner:self];
100 [self setContactList:contactList];
106 - (AIListObject<AIContainingObject> *)contactList
108 return (contactListRoot ? contactListRoot : [contactListController contactList]);
111 - (AIListController *) listController
113 return contactListController;
116 - (AIListOutlineView *)contactListView
118 return contactListView;
121 - (void)setContactList:(AIListObject<AIContainingObject> *)inContactList
123 if (inContactList != contactListRoot) {
124 [contactListRoot release];
125 contactListRoot = [inContactList retain];
129 //Our window nib name
130 + (NSString *)nibName
135 - (Class)listControllerClass
137 return [AIListController class];
142 [searchField setDelegate:nil];
144 [filterBarAnimation stopAnimation];
145 [filterBarAnimation setDelegate:nil];
146 self.filterBarAnimation = nil;
148 [filterBarPreviouslySelected release];
150 [[NSNotificationCenter defaultCenter] removeObserver:self];
152 [windowAnimation stopAnimation];
153 [windowAnimation setDelegate:nil];
154 self.windowAnimation = nil;
156 [contactListController close];
157 [windowLastScreen release];
162 - (NSString *)adiumFrameAutosaveName
164 AILogWithSignature(@"My autosave name is %@",[NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]]);
165 return [NSString stringWithFormat:@"Contact List:%@", [[self contactList] contentsBasedIdentifier]];
168 //Setup the window after it has loaded
169 - (void)windowDidLoad
171 contactListController = [[[self listControllerClass] alloc] initWithContactList:[self contactList]
172 inOutlineView:contactListView
173 inScrollView:scrollView_contactList
176 //super's windowDidLoad will restore our location, which is based upon the contactListRoot
177 [super windowDidLoad];
179 //Exclude this window from the window menu (since we add it manually)
180 [[self window] setExcludedFromWindowsMenu:YES];
181 [[self window] useOptimizedDrawing:YES];
183 minWindowSize = [[self window] minSize];
184 [contactListController setMinWindowSize:minWindowSize];
186 [[self window] setTitle:AILocalizedString(@"Contacts","Contact List window title")];
188 //Watch for resolution and screen configuration changes
189 [[NSNotificationCenter defaultCenter] addObserver:self
190 selector:@selector(screenParametersChanged:)
191 name:NSApplicationDidChangeScreenParametersNotification
195 filterBarExpandedGroups = NO;
196 filterBarIsVisible = NO;
197 filterBarShownAutomatically = NO;
198 self.filterBarAnimation = nil;
199 filterBarPreviouslySelected = nil;
200 [searchField setDelegate:self];
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];
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];
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];
216 [[NSNotificationCenter defaultCenter] addObserver:self
217 selector:@selector(applicationDidUnhide:)
218 name:NSApplicationDidUnhideNotification
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]];
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.
229 [[searchField cell] setCancelButtonCell:nil];
231 [[NSNotificationCenter defaultCenter] addObserver:self
232 selector:@selector(windowDidResignMain:)
233 name:NSWindowDidResignMainNotification
234 object:[self window]];
236 //Save our frame immediately for sliding purposes
237 [self setSavedFrame:[[self window] frame]];
240 //Close the contact list window
241 - (void)windowWillClose:(NSNotification *)notification
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");
248 //Then move it back on screen so that we'll save the proper position in -[AIWindowController windowWillClose:]
249 [self slideWindowOnScreenWithAnimation:NO];
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];
258 [super windowWillClose:notification];
260 //Invalidate the dock-like hiding timer
261 [slideWindowIfNeededTimer invalidate]; [slideWindowIfNeededTimer release];
264 [adium.preferenceController unregisterPreferenceObserver:self];
265 [[NSNotificationCenter defaultCenter] removeObserver:self];
266 [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:self];
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];
274 NSInteger levelForAIWindowLevel(AIWindowLevel windowLevel)
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;
288 - (void)setWindowLevel:(NSInteger)level
290 AILogWithSignature(@"Setting to %i", level);
291 [[self window] setLevel:level];
294 //Preferences have changed
295 - (void)preferencesChangedForGroup:(NSString *)group
297 object:(AIListObject *)object
298 preferenceDict:(NSDictionary *)prefDict
299 firstTime:(BOOL)firstTime
301 BOOL shouldRevealWindowAndDelaySliding = NO;
303 // Make sure we're not getting an object-specific update.
307 if ([group isEqualToString:PREF_GROUP_CONTACT_LIST]) {
308 windowLevel = [[prefDict objectForKey:KEY_CL_WINDOW_LEVEL] integerValue];
309 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
311 listHasShadow = [[prefDict objectForKey:KEY_CL_WINDOW_HAS_SHADOW] boolValue];
312 [[self window] setHasShadow:listHasShadow];
314 windowHidingStyle = [[prefDict objectForKey:KEY_CL_WINDOW_HIDING_STYLE] integerValue];
315 slideOnlyInBackground = [[prefDict objectForKey:KEY_CL_SLIDE_ONLY_IN_BACKGROUND] boolValue];
317 [[self window] setHidesOnDeactivate:(windowHidingStyle == AIContactListWindowHidingStyleBackground)];
319 showOnAllSpaces = [[prefDict objectForKey:KEY_CL_ALL_SPACES] boolValue];
320 [[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
322 if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
323 if (!slideWindowIfNeededTimer) {
324 slideWindowIfNeededTimer = [[NSTimer scheduledTimerWithTimeInterval:DOCK_HIDING_MOUSE_POLL_INTERVAL
326 selector:@selector(slideWindowIfNeeded:)
328 repeats:YES] retain];
331 } else if (slideWindowIfNeededTimer) {
332 [slideWindowIfNeededTimer invalidate];
333 [slideWindowIfNeededTimer release]; slideWindowIfNeededTimer = nil;
336 [contactListController setShowTooltips:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS] boolValue]];
337 [contactListController setShowTooltipsInBackground:[[prefDict objectForKey:KEY_CL_SHOW_TOOLTIPS_IN_BACKGROUND] boolValue]];
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;
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];
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
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;
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;
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;
378 //Show the resize indicator if either or both of the autoresizing options is NO
379 [[self window] setShowsResizeIndicator:!(autoResizeVertically && autoResizeHorizontally)];
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).
385 NSSize thisMinimumSize = minWindowSize;
386 NSSize thisMaximumSize = NSMakeSize(maxWindowWidth, 10000);
387 NSRect currentFrame = [[self window] frame];
389 if (forcedWindowWidth != -1) {
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.
394 if (!(autoResizeVertically || autoResizeHorizontally)) {
395 thisMinimumSize.width = forcedWindowWidth;
396 [[self window] setFrame:NSMakeRect(currentFrame.origin.x,currentFrame.origin.y,forcedWindowWidth,currentFrame.size.height)
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;
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;
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
418 [contactListView setMinimumDesiredWidth:((windowStyle == AIContactListWindowStyleStandard) ? 175 : 0)];
420 [[self window] setMinSize:thisMinimumSize];
421 [[self window] setMaxSize:thisMaximumSize];
423 contactListController.autoResizeHorizontally = autoResizeHorizontally;
424 contactListController.autoResizeVertically = autoResizeVertically;
426 [contactListController setForcedWindowWidth:forcedWindowWidth];
427 [contactListController setMaxWindowWidth:maxWindowWidth];
429 [contactListController contactListDesiredSizeChanged];
432 shouldRevealWindowAndDelaySliding = YES;
437 if ([group isEqualToString:PREF_GROUP_APPEARANCE]) {
438 CGFloat opacity = [[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_OPACITY] doubleValue];
439 [contactListController setBackgroundOpacity:opacity];
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.
445 BOOL forceWindowToCatchMouseEvents = ([[prefDict objectForKey:KEY_LIST_LAYOUT_WINDOW_STYLE] integerValue] != AIContactListWindowStyleContactBubbles_Fitted);
446 if (forceWindowToCatchMouseEvents)
447 [[self window] setIgnoresMouseEvents:NO];
450 shouldRevealWindowAndDelaySliding = YES;
454 if ([group isEqualToString:PREF_GROUP_CONTACT_LIST_DISPLAY]) {
455 [contactListController setUseContactListGroups:![[prefDict objectForKey:KEY_HIDE_CONTACT_LIST_GROUPS] boolValue]];
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];
467 NSInteger iconSize = [[layoutDict objectForKey:KEY_LIST_LAYOUT_USER_ICON_SIZE] integerValue];
468 [AIUserIcons setListUserIconSize:NSMakeSize(iconSize,iconSize)];
472 if (groupTheme || firstTime) {
473 NSString *imagePath = [themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_PATH];
476 if (imagePath && [imagePath length] && [[themeDict objectForKey:KEY_LIST_THEME_BACKGROUND_IMAGE_ENABLED] boolValue]) {
477 [contactListView setBackgroundImage:[[[NSImage alloc] initWithContentsOfFile:imagePath] autorelease]];
479 [contactListView setBackgroundImage:nil];
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];
490 //Both layout and theme
491 [contactListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
494 shouldRevealWindowAndDelaySliding = YES;
498 if (shouldRevealWindowAndDelaySliding) {
499 [self delayWindowSlidingForInterval:2];
500 [self slideWindowOnScreenWithAnimation:NO];
503 //Do a slide immediately if needed (to display as per our new preferneces)
504 [self slideWindowIfNeeded:nil];
509 - (IBAction)performDefaultActionOnSelectedObject:(AIListObject *)selectedObject sender:(NSOutlineView *)sender
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];
517 [sender expandItem:proxyObject];
521 } else if ([selectedObject isMemberOfClass:[AIListBookmark class]]) {
522 //Hide any tooltip the contactListController is currently showing
523 [contactListController hideTooltip];
525 [(AIListBookmark *)selectedObject openChat];
527 } else if ([selectedObject isKindOfClass:[AIListContact class]]) {
528 //Hide any tooltip the contactListController is currently showing
529 [contactListController hideTooltip];
531 //Open a new message with the contact
532 [adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)selectedObject
533 onPreferredAccount:YES]];
537 - (BOOL) canCustomizeToolbar
542 //Interface Container --------------------------------------------------------------------------------------------------
543 #pragma mark Interface Container
544 //Close this container
545 - (void)close:(id)sender
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.
552 if ([self windowShouldClose:nil]) {
553 [[self window] close];
559 - (void)makeActive:(id)sender
561 [[self window] makeKeyAndOrderFront:self];
565 //Contact list brought to front
566 - (void)windowDidBecomeKey:(NSNotification *)notification
568 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactListDidBecomeMain object:self];
571 //Contact list sent back
572 - (void)windowDidResignKey:(NSNotification *)notification
574 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactListDidResignMain object:self];
577 - (void)showWindowInFrontIfAllowed:(BOOL)inFront
579 //Always show for three seconds at least if we're told to show
580 [self delayWindowSlidingForInterval:3];
582 //Call super to actually do the showing
583 [super showWindowInFrontIfAllowed:inFront];
585 NSWindow *window = [self window];
587 if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
588 [self slideWindowOnScreenWithAnimation:NO];
591 windowSlidOffScreenEdgeMask = AINoEdges;
593 currentScreen = [window screen];
594 currentScreenFrame = [currentScreen frame];
596 if ([[NSScreen screens] count] &&
597 (currentScreen == [[NSScreen screens] objectAtIndex:0])) {
598 currentScreenFrame.size.height -= [[NSApp mainMenu] menuBarHeight];
601 //Ensure the window is displaying at the proper level and exposé setting
602 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
605 - (void)setSavedFrame:(NSRect)frame
615 // Auto-resizing support ------------------------------------------------------------------------------------------------
616 #pragma mark Auto-resizing support
618 - (void)respondToScreenParametersChanged:(NSNotification *)notification
620 NSWindow *window = [self window];
622 NSScreen *windowScreen = [window screen];
624 if ([[NSScreen screens] containsObject:windowLastScreen]) {
625 windowScreen = windowLastScreen;
627 [windowLastScreen release]; windowLastScreen = nil;
628 windowScreen = [NSScreen mainScreen];
632 NSRect newScreenFrame = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowScreen]] rectValue];
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];
639 [self delayWindowSlidingForInterval:2];
640 [self slideWindowOnScreenWithAnimation:NO];
644 [contactListController contactListDesiredSizeChanged];
646 currentScreen = [window screen];
647 currentScreenFrame = newScreenFrame;
648 [self setSavedFrame:[window frame]];
651 - (void)screenParametersChanged:(NSNotification *)notification
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
660 #pragma mark Printing
661 - (void)adiumPrint:(id)sender
663 [contactListView print:sender];
666 // Dock-like hiding -----------------------------------------------------------------------------------------------------
667 #pragma mark Dock-like hiding
669 + (void)updateScreenSlideBoundaryRect:(id)sender
671 NSArray *screens = [NSScreen screens];
672 NSInteger numScreens = [screens count];
674 [screenSlideBoundaryRectDictionary release];
675 screenSlideBoundaryRectDictionary = [[NSMutableDictionary alloc] initWithCapacity:numScreens];
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;
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]];
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]];
698 * If the contact list is open but not visible when we unhide, we should always display it; it should not, however, steal focus.
700 - (void)applicationDidUnhide:(NSNotification *)notification
702 if (![[self window] isVisible]) {
703 [self showWindowInFrontIfAllowed:NO];
707 - (BOOL)windowShouldHideOnDeactivate
709 return (windowHidingStyle == AIContactListWindowHidingStyleBackground);
713 * @brief Called on a delay by -[self slideWindowIfNeeded:]
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.
718 - (void)slideWindowOnScreenAfterDelay
720 waitingToSlideOnScreen = NO;
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];
726 [[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
728 overrodeWindowLevel = YES;
731 [self slideWindowOnScreen];
735 * @brief Check what behavior the window should perform and initiate it
737 * Called regularly by a repeating timer to check mouse position against window position.
739 - (void)slideWindowIfNeeded:(id)sender
741 if ([self shouldSlideWindowOnScreen]) {
742 if (!waitingToSlideOnScreen) {
743 [self performSelector:@selector(slideWindowOnScreenAfterDelay)
745 afterDelay:WINDOW_SLIDING_DELAY];
746 waitingToSlideOnScreen = YES;
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.
753 waitingToSlideOnScreen = NO;
754 [[self class] cancelPreviousPerformRequestsWithTarget:self
755 selector:@selector(slideWindowOnScreenAfterDelay)
759 if ([self shouldSlideWindowOffScreen]) {
760 AIRectEdgeMask adjacentEdges = [self slidableEdgesAdjacentToWindow];
762 if (adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask)) {
763 [self slideWindowOffScreenEdges:(adjacentEdges & (AIMinXEdgeMask | AIMaxXEdgeMask))];
765 [self slideWindowOffScreenEdges:adjacentEdges];
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.
771 if (overrodeWindowLevel &&
772 windowHidingStyle == AIContactListWindowHidingStyleSliding) {
773 [self setWindowLevel:kCGBackstopMenuLevel];
775 [[self window] setCollectionBehavior:NSWindowCollectionBehaviorCanJoinAllSpaces];
777 overrodeWindowLevel = YES;
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.
788 [self setWindowLevel:levelForAIWindowLevel(windowLevel)];
790 [[self window] setCollectionBehavior:showOnAllSpaces ? NSWindowCollectionBehaviorCanJoinAllSpaces : NSWindowCollectionBehaviorDefault];
792 overrodeWindowLevel = NO;
797 - (BOOL)shouldSlideWindowOnScreen
799 BOOL shouldSlide = NO;
801 if (([self windowSlidOffScreenEdgeMask] != AINoEdges) &&
803 if (slideOnlyInBackground && [NSApp isActive]) {
804 //We only slide while in the background, and the app is not in the background. Slide on screen.
807 } else if (windowHidingStyle == AIContactListWindowHidingStyleSliding) {
808 //Slide on screen if the mouse position indicates we should
809 shouldSlide = [self shouldSlideWindowOnScreen_mousePositionStrategy];
811 //It's slid off-screen... and it's not supposed to be sliding at all. Slide back on screen!
819 - (BOOL)shouldSlideWindowOffScreen
821 BOOL shouldSlide = NO;
823 if ((windowHidingStyle == AIContactListWindowHidingStyleSliding) &&
825 ([self windowSlidOffScreenEdgeMask] == AINoEdges) &&
826 (!(slideOnlyInBackground && [NSApp isActive]))) {
827 shouldSlide = [self shouldSlideWindowOffScreen_mousePositionStrategy];
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
837 BOOL shouldSlideOffScreen = NO;
839 NSWindow *window = [self window];
840 NSRect windowFrame = [window frame];
841 NSPoint mouseLocation = [NSEvent mouseLocation];
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;
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?
856 NSEventType currentEventType = [[NSApp currentEvent] type];
857 if (currentEventType == NSLeftMouseDragged ||
858 currentEventType == NSRightMouseDragged ||
859 currentEventType == NSOtherMouseDragged ||
860 currentEventType == NSPeriodic) {
861 shouldSlideOffScreen = NO;
864 return shouldSlideOffScreen;
867 // note: may be inaccurate when mouse is up against an edge
868 - (NSScreen *)screenForPoint:(NSPoint)point
870 for (NSScreen *pointScreen in [NSScreen screens]) {
871 if (NSPointInRect(point, NSInsetRect([pointScreen frame], -1, -1)))
877 - (NSRect)squareRectWithCenter:(NSPoint)point sideLength:(CGFloat)sideLength
879 return NSMakeRect(point.x - sideLength*0.5f, point.y - sideLength*0.5f, sideLength, sideLength);
882 - (BOOL)pointIsInScreenCorner:(NSPoint)point
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)
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);
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]));
904 * @brief Should the window be slid on screen given the mouse's position?
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.
908 * @result YES if the mouse is against all edges of the screen where we previously slid the window and not in a corner.
910 - (BOOL)shouldSlideWindowOnScreen_mousePositionStrategy
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;
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.
922 if (NSPointInRect(mouseLocation, screenSlideBoundaryRect)) {
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,
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;
935 mouseNearSlideOffEdges = YES;
941 return mouseNearSlideOffEdges && ![self pointIsInScreenCorner:mouseLocation];
948 #pragma mark Dock-like hiding
950 - (NSScreen *)windowLastScreen
952 return windowLastScreen;
955 - (BOOL)animationShouldStart:(NSAnimation *)animation
957 if(![animation isEqual:windowAnimation])
960 //Whenever an animation starts, we should be using the normal shadow setting
961 [[self window] setHasShadow:listHasShadow];
963 //Don't let docking interfere with the animation
964 if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
965 [(id)[self window] setDockingEnabled:NO];
967 if (windowSlidOffScreenEdgeMask == AINoEdges) {
968 [[self window] setAlphaValue:previousAlpha];
969 AILogWithSignature(@"Set window to previous alpha of %f", previousAlpha);
975 - (void)animationDidEnd:(NSAnimation*)animation
977 if([animation isEqual:windowAnimation]) {
978 //Restore docking behavior
979 if ([[self window] respondsToSelector:@selector(setDockingEnabled:)])
980 [(id)[self window] setDockingEnabled:YES];
982 if (windowSlidOffScreenEdgeMask == AINoEdges) {
983 //When the window is offscreen, its horizontal autosizing can't occur. Size it now.
984 [contactListController contactListDesiredSizeChanged];
987 //Offscreen windows should be told not to cast a shadow
988 [[self window] setHasShadow:NO];
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);
995 self.windowAnimation = nil;
998 if (animation == filterBarAnimation) {
999 if (filterBarIsVisible) {
1000 // If the filter bar is already visible, remove it from its superview.
1001 [filterBarView removeFromSuperview];
1003 // Set the first responder back to the contact list view.
1004 [[self window] makeFirstResponder:contactListView];
1006 [contactListView selectItemsInArray:filterBarPreviouslySelected];
1008 // Since this wasn't a user-initiated selection change, we need to post a notification for it.
1009 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactSelectionChanged
1012 [filterBarPreviouslySelected release]; filterBarPreviouslySelected = nil;
1014 filterBarIsVisible = NO;
1016 // If the filter bar wasn't visible, make it the first responder.
1017 [[self window] makeFirstResponder:searchField];
1019 // Set the filter bar as the next responder so the chain works for things like the info inspector
1020 [filterBarView setNextResponder:contactListView];
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];
1025 filterBarPreviouslySelected = [[contactListView arrayOfSelectedItems] retain];
1027 filterBarIsVisible = YES;
1030 // Let the contact list controller know that our size has changed.
1031 [contactListController contactListDesiredSizeChanged];
1033 // We're no longer animating.
1034 self.filterBarAnimation = nil;
1038 - (BOOL)keepListOnScreenWhenSliding
1044 * @brief Slide the window to a given point
1046 * windowSlidOffScreenEdgeMask must already be set to the resulting offscreen mask (or 0 if the window is sliding on screen)
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.
1051 - (void)slideWindowToPoint:(NSPoint)targetPoint
1053 NSWindow *myWindow = [self window];
1054 NSScreen *windowScreen;
1056 windowScreen = [myWindow screen];
1057 if (!windowScreen) windowScreen = [self windowLastScreen];
1058 if (!windowScreen) windowScreen = [NSScreen mainScreen];
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;
1065 frame.origin = targetPoint;
1067 if ((windowSlidOffScreenEdgeMask != AINoEdges) &&
1068 [self keepListOnScreenWhenSliding]) {
1069 switch (windowSlidOffScreenEdgeMask) {
1070 case AIMinXEdgeMask:
1071 frame.origin.x += 1;
1073 case AIMaxXEdgeMask:
1074 frame.origin.x -= 1;
1076 case AIMaxYEdgeMask:
1077 frame.origin.y -= 1;
1079 case AIMinYEdgeMask:
1080 frame.origin.y += 1;
1083 //We'll never get here
1088 if (windowAnimation) {
1089 [windowAnimation stopAnimation];
1090 self.windowAnimation = nil;
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];
1106 - (void)moveWindowToPoint:(NSPoint)inOrigin
1108 [[self window] setFrameOrigin:inOrigin];
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.
1114 [contactListController contactListDesiredSizeChanged];
1115 [[self window] setAlphaValue:previousAlpha];
1116 AILogWithSignature(@"Set window to previous alpha of %f", previousAlpha);
1120 static BOOL AIScreenRectEdgeAdjacentToAnyOtherScreen(NSRectEdge edge, NSScreen *screen)
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;
1131 shiftedScreenFrame.origin.x -= 1;
1134 shiftedScreenFrame.origin.y -= 1;
1137 shiftedScreenFrame.size.width += 1;
1140 shiftedScreenFrame.size.height += 1;
1144 for (NSInteger i = 0; i < numScreens; i++) {
1145 NSScreen *otherScreen = [screens objectAtIndex:i];
1146 if (otherScreen != screen) {
1147 if (NSIntersectsRect([otherScreen frame], shiftedScreenFrame)) {
1162 * @brief Find the mask specifying what edges are potentially slidable for our window
1164 * @result AIRectEdgeMask, which is 0 if no edges are slidable
1166 - (AIRectEdgeMask)slidableEdgesAdjacentToWindow
1168 AIRectEdgeMask slidableEdges = 0;
1170 NSWindow *window = [self window];
1171 NSRect windowFrame = [window frame];
1172 NSScreen *windowScreen = [window screen];
1173 NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowScreen]] rectValue];
1176 for (edge = 0; edge < 4; edge++) {
1177 if ((SLIDE_ALLOWED_RECT_EDGE_MASK & (1 << edge)) &&
1178 (AIRectIsAligned_edge_toRect_edge_tolerance_(windowFrame,
1180 screenSlideBoundaryRect,
1182 WINDOW_ALIGNMENT_TOLERANCE)) &&
1183 (!AIScreenRectEdgeAdjacentToAnyOtherScreen(edge, windowScreen))) {
1184 slidableEdges |= (1 << edge);
1188 return slidableEdges;
1191 - (void)slideWindowOffScreenEdges:(AIRectEdgeMask)rectEdgeMask
1194 NSRect newWindowFrame;
1197 if (rectEdgeMask == AINoEdges)
1200 window = [self window];
1201 newWindowFrame = [window frame];
1203 [self setSavedFrame:newWindowFrame];
1205 [windowLastScreen release];
1206 windowLastScreen = [[window screen] retain];
1208 NSRect screenSlideBoundaryRect = [[screenSlideBoundaryRectDictionary objectForKey:[NSValue valueWithNonretainedObject:windowLastScreen]] rectValue];
1210 for (edge = 0; edge < 4; edge++) {
1211 if (rectEdgeMask & (1 << edge)) {
1212 newWindowFrame = AIRectByAligningRect_edge_toRect_edge_(newWindowFrame,
1213 AIOppositeRectEdge_(edge),
1214 screenSlideBoundaryRect,
1219 windowSlidOffScreenEdgeMask |= rectEdgeMask;
1221 [self slideWindowToPoint:newWindowFrame.origin];
1224 - (void)slideWindowOnScreenWithAnimation:(BOOL)animate
1226 if ([self windowSlidOffScreenEdgeMask] != AINoEdges) {
1227 NSWindow *window = [self window];
1228 NSRect windowFrame = [window frame];
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];
1237 windowSlidOffScreenEdgeMask = AINoEdges;
1240 [self slideWindowToPoint:oldFrame.origin];
1242 [self moveWindowToPoint:oldFrame.origin];
1245 [windowLastScreen release]; windowLastScreen = nil;
1250 - (void)slideWindowOnScreen
1252 [self slideWindowOnScreenWithAnimation:YES];
1255 - (void)setPreventHiding:(BOOL)newPreventHiding {
1256 preventHiding = newPreventHiding;
1259 - (void)endWindowSlidingDelay
1261 [self setPreventHiding:NO];
1264 - (void)delayWindowSlidingForInterval:(NSTimeInterval)inDelayTime
1266 [self setPreventHiding:YES];
1268 [NSObject cancelPreviousPerformRequestsWithTarget:self
1269 selector:@selector(endWindowSlidingDelay)
1271 [self performSelector:@selector(endWindowSlidingDelay)
1273 afterDelay:inDelayTime];
1276 - (AIRectEdgeMask)windowSlidOffScreenEdgeMask
1278 return windowSlidOffScreenEdgeMask;
1281 // Snap Groups Together------------------------------------------------------------------------------------------------
1282 #pragma mark Snap Groups Together
1285 * @brief If window did move and is not docked then snap it to other windows
1287 - (void)windowDidMove:(NSNotification *)notification
1289 BOOL suppressSnapping = [NSEvent shiftKey];
1291 attachToBottom = nil;
1293 if (windowSlidOffScreenEdgeMask == AINoEdges && !suppressSnapping)
1294 [self snapToOtherWindows];
1298 * @brief Captures mouse up event to check that if the window snapped underneath
1299 * another window they are merged together
1301 - (void)mouseUp:(NSEvent *)event {
1302 if (attachToBottom) {
1303 AIContactList *from = (AIContactList *)[self contactList];
1304 AIContactList *to = (AIContactList *)[attachToBottom contactList];
1306 for (AIListGroup *group in from) {
1307 [adium.contactController moveGroup:group fromContactList:from toContactList:to];
1310 [[NSNotificationCenter defaultCenter] postNotificationName:DetachedContactListIsEmpty
1313 [[NSNotificationCenter defaultCenter] postNotificationName:@"Contact_ListChanged"
1318 [super mouseUp:event];
1322 * @brief Snaps window to windows next to it
1324 - (void)snapToOtherWindows
1326 NSWindow *myWindow = [self window];
1327 NSArray *windows = [[NSApplication sharedApplication] windows];
1331 NSRect currentFrame = [myWindow frame];
1332 NSPoint suggested = currentFrame.origin;
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.
1343 suggested = [self snapTo:window with:currentFrame saveTo:suggested];
1347 [[self window] setFrameOrigin:suggested];
1352 * @brief Check that window is inside snappable region of other window
1354 static BOOL isInRangeOfRect(NSRect sourceRect, NSRect targetRect)
1356 return NSIntersectsRect(NSInsetRect(sourceRect, -SNAP_DISTANCE, -SNAP_DISTANCE), targetRect);
1360 * @brief Check if points are close enough to be snapped together
1362 static BOOL canSnap(CGFloat a, CGFloat b)
1364 return (abs(a - b) <= SNAP_DISTANCE);
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;
1373 if (!NSEqualRects(neighbor,currentRect) && isInRangeOfRect(currentRect, neighbor)) {
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);
1386 if (canSnap(NSMaxY(neighbor), NSMaxY(currentRect))) {
1387 location.y = NSMaxY(neighbor) - NSHeight(currentRect);
1389 } else if (canSnap(NSMinY(neighbor), NSMaxY(currentRect))) {
1390 location.y = NSMinY(neighbor) - NSHeight(currentRect) - spacing.y;
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);
1401 // If we snapped on top of neighbor
1403 return currentRect.origin;
1405 // Save window that we could possible attach to
1407 attachToBottom = [neighborWindow delegate];
1414 * @brief Gets space that windows should be apart by based on current window style
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];
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);
1432 return NSMakePoint(0,0);
1435 #pragma mark Filtering
1437 * @brief Toggles the find bar on, or brings it into focus if it is already visible
1439 - (void)toggleFindPanel:(id)sender;
1441 if (filterBarIsVisible) {
1442 [[self window] makeFirstResponder:searchField];
1444 } else if ([contactListView numberOfRows] > 0) {
1445 filterBarShownAutomatically = NO;
1446 [self showFilterBarWithAnimation:YES];
1454 * @brief Hide the filter bar
1456 - (IBAction)hideFilterBar:(id)sender;
1458 [self hideFilterBarWithAnimation:YES];
1462 * @brief Show the filter bar
1464 * @param useAnimation If YES, the filter bar will scroll into view, otherwise it appears immediately
1466 - (void)showFilterBarWithAnimation:(BOOL)useAnimation
1468 if (filterBarIsVisible || filterBarAnimation)
1471 // While the filter bar is shown, temporarily disable automatic horizontal resizing
1472 contactListController.autoResizeHorizontally = NO;
1474 // Disable contact list animation while the filter bar is shown
1475 [contactListView setEnableAnimation:NO];
1477 // Animate the filter bar into view
1478 [self animateFilterBarWithDuration:(useAnimation ? 0.15f : 0.0)];
1482 * @brief Hide the filter bar
1484 * @param useAnimation If YES, the filter bar will scroll out of view, otherwise it disappears immediately
1486 - (void)hideFilterBarWithAnimation:(BOOL)useAnimation
1488 if (!filterBarIsVisible || filterBarAnimation)
1491 // Clear the search field so that visibility is reset
1492 [searchField setStringValue:@""];
1493 [self filterContacts:searchField];
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];
1498 [contactListView setEnableAnimation:[[adium.preferenceController preferenceForKey:KEY_CL_ANIMATE_CHANGES
1499 group:PREF_GROUP_CONTACT_LIST] boolValue]];
1501 // Animate the filter bar out of view
1502 [self animateFilterBarWithDuration:(useAnimation ? 0.15f : 0.0)];
1506 * @brief Animates the filter bar in and out of view
1508 * @param duration The duration the animation will last
1510 - (void)animateFilterBarWithDuration:(CGFloat)duration
1512 NSView *targetView = ([contactListView enclosingScrollView] ? (NSView *)[contactListView enclosingScrollView] : contactListView);
1513 NSRect targetFrame = [targetView frame];
1514 NSDictionary *targetViewDict, *filterBarDict;
1516 // Contact list resizing
1517 if (filterBarIsVisible) {
1518 targetFrame.size.height = NSHeight(targetFrame) + NSHeight([filterBarView bounds]);
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.
1526 if (NSHeight(targetFrame) < (NSHeight([filterBarView bounds]) * 2)) {
1527 NSRect windowFrame = [[targetView window] frame];
1529 [[targetView window] setFrame:NSMakeRect(NSMinX(windowFrame), NSMinY(windowFrame) - NSHeight([filterBarView bounds]),
1530 NSWidth(windowFrame), NSHeight(windowFrame) + NSHeight([filterBarView bounds]))
1534 targetFrame = [targetView frame];
1537 targetFrame.size.height = NSHeight(targetFrame) - NSHeight([filterBarView bounds]);
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.
1544 if (targetFrame.size.height == 0)
1545 targetFrame.size.height = 1;
1547 // Filter bar resizing
1548 NSRect barTargetFrame = contactListView.enclosingScrollView.frame;
1549 if (filterBarIsVisible) {
1550 barTargetFrame.size.height = NSHeight(barTargetFrame) + NSHeight(filterBarView.bounds);
1552 barTargetFrame.size.height = NSHeight(barTargetFrame) - NSHeight(filterBarView.bounds);
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]))];
1562 // Attach the filter bar to the window
1563 [[[self window] contentView] addSubview:filterBarView];
1566 filterBarDict = [NSDictionary dictionaryWithObjectsAndKeys:filterBarView, NSViewAnimationTargetKey,
1567 [NSValue valueWithRect:NSMakeRect(NSMinX(barTargetFrame), NSHeight(barTargetFrame),
1568 NSWidth(barTargetFrame), NSHeight([filterBarView bounds]))], NSViewAnimationEndFrameKey, nil];
1570 targetViewDict = [NSDictionary dictionaryWithObjectsAndKeys:targetView, NSViewAnimationTargetKey,
1571 [NSValue valueWithRect:targetFrame], NSViewAnimationEndFrameKey, nil];
1573 self.filterBarAnimation = [[[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObjects:
1577 [filterBarAnimation setDuration:duration];
1578 [filterBarAnimation setAnimationBlockingMode:NSAnimationBlocking];
1579 [filterBarAnimation setDelegate:self];
1581 // Start the animation
1582 [filterBarAnimation startAnimation];
1586 * @brief Called when the window loses focus
1588 - (void)windowDidResignMain:(NSNotification *)sender
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.
1593 if (filterBarIsVisible && filterBarShownAutomatically)
1594 [self hideFilterBarWithAnimation:NO];
1598 * @brief Forward typing events from the contact list to the filter bar
1600 - (BOOL)forwardKeyEventToFindPanel:(NSEvent *)theEvent;
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;
1606 //Get the pressed character
1607 if ([charString length] == 1) pressedChar = [charString characterAtIndex:0];
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.
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)))) {
1620 if (!filterBarIsVisible) {
1621 /* Typing caused the filter bar ot be shown automatically */
1622 filterBarShownAutomatically = YES;
1623 [self showFilterBarWithAnimation:NO];
1626 [[self window] makeFirstResponder:searchField];
1627 [[[self window] fieldEditor:YES forObject:searchField] keyDown:theEvent];
1634 * @brief Process text commands while on the search field
1636 - (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
1638 // Only process commands when we're in the search field.
1639 if (control != searchField)
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];
1656 // If we didn't process a command, return NO.
1660 // We processed a command, return YES.
1665 * @brief Filter contacts from the search field
1667 * This method will expand or contract groups as necessary, as well as handle forwarding the search term to
1668 * the contact hiding controller.
1670 - (IBAction)filterContacts:(id)sender;
1672 if (![sender isKindOfClass:[NSSearchField class]])
1675 if (!filterBarExpandedGroups && ![[sender stringValue] isEqualToString:@""]) {
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];
1684 filterBarExpandedGroups = YES;
1687 [contactListView reloadData];
1689 } else if (filterBarExpandedGroups && [[sender stringValue] isEqualToString:@""]) {
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];
1698 filterBarExpandedGroups = NO;
1701 [contactListView reloadData];
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];
1710 // Since this wasn't a user-initiated selection change, we need to post a notification for it.
1711 [[NSNotificationCenter defaultCenter] postNotificationName:Interface_ContactSelectionChanged
1714 [[searchField cell] setTextColor:nil backgroundColor:nil];
1717 //White on light red (like Firefox!)
1718 [[searchField cell] setTextColor:[NSColor whiteColor] backgroundColor:[NSColor colorWithCalibratedHue:0.983
1726 * @brief Delegate method for the search field's close button
1728 - (void)rolloverButton:(AIRolloverButton *)inButton mouseChangedToInsideButton:(BOOL)isInside
1730 [button_cancelFilterBar setImage:[NSImage imageNamed:(isInside ? @"FTProgressStopRollover" : @"FTProgressStop")
1731 forClass:[self class]]];