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 "AIListController.h"
18 #import "AIAnimatingListOutlineView.h"
19 #import "AIListWindowController.h"
20 #import <Adium/AIChat.h>
21 #import <Adium/AIChatControllerProtocol.h>
22 #import <Adium/AIContactControllerProtocol.h>
23 #import <Adium/AIContentControllerProtocol.h>
24 #import <Adium/AIContentMessage.h>
25 #import <Adium/AIInterfaceControllerProtocol.h>
26 #import <Adium/AIPreferenceControllerProtocol.h>
27 #import <Adium/AISortController.h>
28 #import <Adium/ESFileTransfer.h>
29 #import <Adium/AIListContact.h>
30 #import <Adium/AIListGroup.h>
31 #import <Adium/AIListObject.h>
32 #import <Adium/AIListOutlineView.h>
33 #import <AIUtilities/AIAttributedStringAdditions.h>
34 #import <AIUtilities/AIAutoScrollView.h>
35 #import <AIUtilities/AIWindowAdditions.h>
36 #import <AIUtilities/AIOutlineViewAdditions.h>
37 #import <AIUtilities/AIObjectAdditions.h>
38 #import <AIUtilities/AIFunctions.h>
40 #define EDGE_CATCH_X 40.0f
41 #define EDGE_CATCH_Y 40.0f
43 #define MENU_BAR_HEIGHT 22
45 #define KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN [NSString stringWithFormat:@"Contact List Docked To Bottom:%@", [[self contactList] contentsBasedIdentifier]]
47 #define PREF_GROUP_APPEARANCE @"Appearance"
49 @interface AIListController (PRIVATE)
50 - (void)contactListChanged:(NSNotification *)notification;
53 @implementation AIListController
56 - (id)initWithContactList:(AIListObject<AIContainingObject> *)aContactList
57 inOutlineView:(AIListOutlineView *)inContactListView
58 inScrollView:(AIAutoScrollView *)inScrollView_contactList
59 delegate:(id<AIListControllerDelegate>)inDelegate
61 if ((self = [self initWithContactListView:inContactListView inScrollView:inScrollView_contactList delegate:inDelegate])) {
62 [contactListView setDrawHighlightOnlyWhenMain:YES];
64 autoResizeVertically = NO;
65 autoResizeHorizontally = NO;
66 maxWindowWidth = 10000;
67 forcedWindowWidth = -1;
69 //Observe contact list content and display changes
70 [[adium notificationCenter] addObserver:self selector:@selector(contactListChanged:)
71 name:Contact_ListChanged
73 [[adium notificationCenter] addObserver:self selector:@selector(contactOrderChanged:)
74 name:Contact_OrderChanged
77 [contactListView addObserver:self
78 forKeyPath:@"desiredHeight"
79 options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
82 [self setContactListRoot:(aContactList ? aContactList : [[adium contactController] contactList])];
84 //Recall how the contact list was docked last time Adium was open
85 dockToBottomOfScreen = [[[adium preferenceController] preferenceForKey:KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN
86 group:PREF_GROUP_WINDOW_POSITIONS] intValue];
88 //Observe preference changes
89 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST];
95 //Setup the window after it has loaded
96 - (void)configureViewsAndTooltips
98 [super configureViewsAndTooltips];
100 //Listen to when the list window moves (so we can remember which edge we're docked to)
101 [[NSNotificationCenter defaultCenter] addObserver:self
102 selector:@selector(windowDidMove:)
103 name:NSWindowDidMoveNotification
104 object:[contactListView window]];
110 [[adium notificationCenter] removeObserver:self];
111 [[NSNotificationCenter defaultCenter] removeObserver:self];
112 [[adium preferenceController] unregisterPreferenceObserver:self];
119 [contactListView removeObserver:self forKeyPath:@"desiredHeight"];
125 - (void)preferencesChangedForGroup:(NSString *)group
127 object:(AIListObject *)object
128 preferenceDict:(NSDictionary *)prefDict
129 firstTime:(BOOL)firstTime
131 [(AIAnimatingListOutlineView *)contactListView setEnableAnimation:[[prefDict objectForKey:KEY_CL_ANIMATE_CHANGES] boolValue]];
134 //Resizing And Positioning ---------------------------------------------------------------------------------------------
135 #pragma mark Resizing And Positioning
136 //Dynamically resize the contact list
137 - (void)contactListDesiredSizeChanged
141 if ((autoResizeVertically || autoResizeHorizontally) &&
142 (theWindow = [contactListView window]) &&
143 [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AINoEdges) {
145 NSRect currentFrame = [theWindow frame];
146 NSRect desiredFrame = [self _desiredWindowFrameUsingDesiredWidth:(autoResizeHorizontally || (forcedWindowWidth != -1))
147 desiredHeight:autoResizeVertically];
149 if (!NSEqualRects(currentFrame, desiredFrame)) {
150 //We must set the min/max first, otherwise our setFrame will be restricted by them and not produce the
152 float toolbarHeight = (autoResizeVertically ? [theWindow toolbarHeight] : 0);
154 [theWindow setMinSize:NSMakeSize((autoResizeHorizontally ? desiredFrame.size.width : minWindowSize.width),
155 (autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : minWindowSize.height))];
156 [theWindow setMaxSize:NSMakeSize((autoResizeHorizontally ? desiredFrame.size.width : 10000),
157 (autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : 10000))];
159 [theWindow setFrame:desiredFrame display:YES animate:NO];
164 //Size for window zoom
165 - (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame
167 return [self _desiredWindowFrameUsingDesiredWidth:YES desiredHeight:YES];
170 //Window moved, remember which side the user has docked it to
171 - (void)windowDidMove:(NSNotification *)notification
173 NSWindow *theWindow = [contactListView window];
174 NSRect windowFrame = [theWindow frame];
175 NSScreen *theWindowScreen = [theWindow screen];
177 NSRect boundingFrame = [theWindowScreen frame];
178 NSRect visibleBoundingFrame = [theWindowScreen visibleFrame];
180 AIDockToBottomType oldDockToBottom = dockToBottomOfScreen;
182 //First, see if they are now within EDGE_CATCH_Y of the total boundingFrame
183 if ((windowFrame.origin.y < boundingFrame.origin.y + EDGE_CATCH_Y) &&
184 ((windowFrame.origin.y + windowFrame.size.height) < (boundingFrame.origin.y + boundingFrame.size.height - EDGE_CATCH_Y))) {
185 dockToBottomOfScreen = AIDockToBottom_TotalFrame;
187 //Then, check for the (possibly smaller) visibleBoundingFrame
188 if ((windowFrame.origin.y < visibleBoundingFrame.origin.y + EDGE_CATCH_Y) &&
189 ((windowFrame.origin.y + windowFrame.size.height) < (visibleBoundingFrame.origin.y + visibleBoundingFrame.size.height - EDGE_CATCH_Y))) {
190 dockToBottomOfScreen = AIDockToBottom_VisibleFrame;
192 dockToBottomOfScreen = AIDockToBottom_No;
196 //Remember how the contact list is currently docked for next time
197 if (oldDockToBottom != dockToBottomOfScreen) {
198 [[adium preferenceController] setPreference:[NSNumber numberWithInt:dockToBottomOfScreen]
199 forKey:KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN
200 group:PREF_GROUP_WINDOW_POSITIONS];
204 //Desired frame of our window - if one of the BOOL values is NO, don't modify that value from the current frame
205 - (NSRect)_desiredWindowFrameUsingDesiredWidth:(BOOL)useDesiredWidth desiredHeight:(BOOL)useDesiredHeight
207 NSRect windowFrame, viewFrame, newWindowFrame, screenFrame, visibleScreenFrame, boundingFrame;
208 NSWindow *theWindow = [contactListView window];
209 NSScreen *currentScreen = [theWindow screen];
210 int desiredHeight = [contactListView desiredHeight];
211 BOOL anchorToRightEdge = NO;
213 windowFrame = [theWindow frame];
214 newWindowFrame = windowFrame;
215 viewFrame = [scrollView_contactList frame];
217 if (!currentScreen) currentScreen = [NSScreen mainScreen];
219 screenFrame = [currentScreen frame];
220 visibleScreenFrame = [currentScreen visibleFrame];
223 if (useDesiredWidth) {
224 if (forcedWindowWidth != -1) {
225 //If auto-sizing is disabled, use the specified width
226 newWindowFrame.size.width = forcedWindowWidth;
228 /* Using horizontal auto-sizing, so find and determine our new width
230 * First, subtract the current size of the view from our frame
232 newWindowFrame.size.width -= viewFrame.size.width;
234 //Now, figure out how big the view wants to be and add that to our frame
235 newWindowFrame.size.width += [contactListView desiredWidth];
237 //Don't get bigger than our maxWindowWidth
238 if (newWindowFrame.size.width > maxWindowWidth) {
239 newWindowFrame.size.width = maxWindowWidth;
240 } else if (newWindowFrame.size.width < 0) {
241 newWindowFrame.size.width = 0;
245 //Anchor to the appropriate screen edge
246 anchorToRightEdge = ((currentScreen && ((NSMaxX(windowFrame) + EDGE_CATCH_X) >= NSMaxX(visibleScreenFrame))) ||
247 [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AIMaxXEdgeMask);
248 if (anchorToRightEdge) {
249 newWindowFrame.origin.x = NSMaxX(windowFrame) - NSWidth(newWindowFrame);
251 newWindowFrame.origin.x = NSMinX(windowFrame);
256 * Compute boundingFrame for window
258 * If the window is against the left or right edges of the screen AND the user did not dock to the visibleFrame last,
259 * we use the full screenFrame as our bound.
260 * The edge check is used since most users' docks will not extend to the edges of the screen.
261 * Alternately, if the user docked to the total frame last, we can safely use the full screen even if we aren't
264 BOOL windowOnEdge = ((NSMinX(newWindowFrame) < NSMinX(screenFrame) + EDGE_CATCH_X) ||
265 (NSMaxX(newWindowFrame) > (NSMaxX(screenFrame) - EDGE_CATCH_X)));
267 if ((windowOnEdge && (dockToBottomOfScreen != AIDockToBottom_VisibleFrame)) ||
268 (dockToBottomOfScreen == AIDockToBottom_TotalFrame)) {
271 boundingFrame = screenFrame;
273 //We still should not violate the menuBar, so account for it here if we are on the menuBar screen.
274 if ((screens = [NSScreen screens]) &&
276 (currentScreen == [screens objectAtIndex:0])) {
277 boundingFrame.size.height -= MENU_BAR_HEIGHT;
281 boundingFrame = visibleScreenFrame;
285 if (useDesiredHeight) {
286 //Subtract the current size of the view from our frame
287 newWindowFrame.size.height -= viewFrame.size.height;
289 //Now, figure out how big the view wants to be and add that to our frame
290 newWindowFrame.size.height += desiredHeight;
292 //Vertical positioning and size if we are placed on a screen
293 if (NSHeight(newWindowFrame) >= NSHeight(boundingFrame)) {
294 //If the window is bigger than the screen, keep it on the screen
295 newWindowFrame.size.height = NSHeight(boundingFrame);
296 newWindowFrame.origin.y = NSMinY(boundingFrame);
298 //A non-full height window is anchored to the appropriate screen edge
299 if (dockToBottomOfScreen == AIDockToBottom_No) {
300 //If the user did not dock to the bottom in any way last, the origin should move up
301 newWindowFrame.origin.y = NSMaxY(windowFrame) - NSHeight(newWindowFrame);
303 //If the user did dock (either to the full screen or the visible screen), the origin should remain in place.
304 newWindowFrame.origin.y = NSMinY(windowFrame);
308 //We must never request a height of 0 or OS X will completely move us off the screen
309 if (newWindowFrame.size.height == 0) newWindowFrame.size.height = 1;
311 //Keep the window from hanging off any Y screen edge (This is optional and could be removed if this annoys people)
312 if (NSMaxY(newWindowFrame) > NSMaxY(boundingFrame)) newWindowFrame.origin.y = NSMaxY(boundingFrame) - newWindowFrame.size.height;
313 if (NSMinY(newWindowFrame) < NSMinY(boundingFrame)) newWindowFrame.origin.y = NSMinY(boundingFrame);
316 if (useDesiredWidth) {
317 /* If the desired height plus any toolbar height exceeds the height we determined, we will be showing a scroller;
318 * expand horizontally to take that into account. The magic number 2 fixes this method for use with our borderless
319 * windows... I'm not sure why it's needed, but it doesn't hurt anything.
321 if (desiredHeight + (NSHeight(windowFrame) - NSHeight(viewFrame)) > NSHeight(newWindowFrame) + 2) {
322 float scrollerWidth = [NSScroller scrollerWidthForControlSize:[[scrollView_contactList verticalScroller] controlSize]];
323 newWindowFrame.size.width += scrollerWidth;
325 if (anchorToRightEdge) {
326 newWindowFrame.origin.x -= scrollerWidth;
330 //We must never request a width of 0 or OS X will completely move us off the screen
331 if (newWindowFrame.size.width == 0) newWindowFrame.size.width = 1;
333 //Keep the window from hanging off any X screen edge (This is optional and could be removed if this annoys people)
334 if (NSMaxX(newWindowFrame) > NSMaxX(boundingFrame)) newWindowFrame.origin.x = NSMaxX(boundingFrame) - NSWidth(newWindowFrame);
335 if (NSMinX(newWindowFrame) < NSMinX(boundingFrame)) newWindowFrame.origin.x = NSMinX(boundingFrame);
338 return newWindowFrame;
341 - (void)setMinWindowSize:(NSSize)inSize {
342 minWindowSize = inSize;
344 - (void)setMaxWindowWidth:(int)inWidth {
345 maxWindowWidth = inWidth;
347 - (void)setAutoresizeHorizontally:(BOOL)flag {
348 autoResizeHorizontally = flag;
350 - (void)setAutoresizeVertically:(BOOL)flag {
351 autoResizeVertically = flag;
353 - (void)setForcedWindowWidth:(int)inWidth {
354 forcedWindowWidth = inWidth;
357 //Content Updating -----------------------------------------------------------------------------------------------------
358 #pragma mark Content Updating
360 * @brief The entire contact list, or an entire group, changed
362 * This indicates that an entire group changed -- the contact list is just a giant group, so that includes the entire
363 * contact list changing. Reload the appropriate object.
365 - (void)contactListChanged:(NSNotification *)notification
367 id object = [notification object];
369 //Redisplay and resize
370 if (!object || object == contactList) {
371 [contactListView reloadData];
374 NSDictionary *userInfo = [notification userInfo];
375 AIListGroup *containingGroup = [userInfo objectForKey:@"ContainingGroup"];
377 if (!containingGroup || containingGroup == contactList) {
378 //Reload the whole tree if the containing group is our root
381 /* We need to reload the contaning group since this notification is posted when adding and removing objects.
382 * Reloading the actual object that changed will produce no results since it may not be on the list.
384 [contactListView reloadItem:containingGroup reloadChildren:YES];
389 - (AIListObject<AIContainingObject> *)contactList
394 - (AIListOutlineView *)contactListView
396 return contactListView;
400 * @brief Order of contacts changed
402 * The notification's object is the contact whose order changed.
403 * We must reload the group containing that contact in order to correctly update the list.
405 - (void)contactOrderChanged:(NSNotification *)notification
407 id object = [[notification object] containingObject];
409 //Treat a nil object as equivalent to the whole contact list
410 if (!object || (object == contactList)) {
411 [contactListView reloadData];
413 [contactListView reloadItem:object reloadChildren:YES];
418 * @brief List object attributes changed
420 * Resize horizontally if desired and the display name changed
422 - (void)listObjectAttributesChanged:(NSNotification *)notification
426 [super listObjectAttributesChanged:notification];
428 keys = [[notification userInfo] objectForKey:@"Keys"];
430 //Resize the contact list horizontally
431 if (autoResizeHorizontally) {
432 if (([keys containsObject:@"Display Name"] || [keys containsObject:@"Long Display Name"])) {
433 [self contactListDesiredSizeChanged];
439 * @brief The outline view selection changed
441 * On the next run loop, post Interface_ContactSelectionChanged. Why wait for the next run loop?
442 * If we post this notification immediately, our outline view may not yet be key, and the contact controller
443 * will return nil for 'selectedListObject'. If we wait, the outline view will be definitely be set as key, and
444 * everything will work as expected.
446 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
448 [[adium notificationCenter] performSelector:@selector(postNotificationName:object:)
449 withObject:Interface_ContactSelectionChanged
454 #pragma mark Drag & Drop
457 * @brief Method to check if operations need to be performed
459 - (NSDragOperation)outlineView:(NSOutlineView*)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(int)index
461 NSArray *types = [[info draggingPasteboard] types];
462 NSDragOperation retVal = NSDragOperationNone;
464 //No dropping into contacts
465 BOOL allowBetweenContactDrop = (index == NSOutlineViewDropOnItemIndex);
467 if ([types containsObject:@"AIListObject"]) {
468 if (index != NSOutlineViewDropOnItemIndex && (![[[adium contactController] activeSortController] canSortManually])) {
469 //Don't drag if automatic sort is on
470 //disable drop between for non-Manual Sort.
471 return NSDragOperationNone;
474 NSEnumerator *enumerator = [dragItems objectEnumerator];
476 BOOL hasGroup = NO, hasNonGroup = NO;
477 while ((dragItem = [enumerator nextObject])) {
478 if ([dragItem isKindOfClass:[AIListGroup class]])
480 if (![dragItem isKindOfClass:[AIListGroup class]])
482 if (hasGroup && hasNonGroup) break;
485 //Don't allow a drop within the contact list or within a group if we contain a mixture of groups and non-groups (e.g. contacts)
486 if (hasGroup && hasNonGroup) return NSDragOperationNone;
488 id primaryDragItem = [dragItems objectAtIndex:0];
490 if ([primaryDragItem isKindOfClass:[AIListGroup class]]) {
491 //Disallow dragging groups into or onto other objects
493 if ([item isKindOfClass:[AIListGroup class]]) {
494 // In between objects
495 [outlineView setDropItem:nil dropChildIndex:[[item containingObject] indexOfObject:item]];
497 // On top of an object
498 [outlineView setDropItem:nil dropChildIndex:[[[item containingObject] containingObject] indexOfObject:[item containingObject]]];
503 //We have one or more contacts. Don't allow them to drop on the contact list itself
504 if (!item && [[adium contactController] useContactListGroups]) {
505 /* The user is hovering on the contact list itself.
506 * If groups are shown at all, assuming we have any items in the list at all, she is hovering just below
507 * a group or an item in a group.
508 * Do this right by shifting the drop to object above.
510 id itemAboveProposedIndex = [outlineView itemAtRow:(index - 1)];
511 if (!itemAboveProposedIndex) {
512 //At the very end, presumably
513 itemAboveProposedIndex = [outlineView itemAtRow:([outlineView numberOfRows] - 1)];
516 if ([itemAboveProposedIndex isKindOfClass:[AIListGroup class]]) {
517 [outlineView setDropItem:itemAboveProposedIndex dropChildIndex:NSOutlineViewDropOnItemIndex];
519 [outlineView setDropItem:[itemAboveProposedIndex containingObject] dropChildIndex:NSOutlineViewDropOnItemIndex];
525 if ((index == NSOutlineViewDropOnItemIndex) && [item isKindOfClass:[AIListContact class]] && ([info draggingSource] == [self contactListView])) {
526 //Dropping into a contact or attaching groups: Copy
527 if (([contactListView rowForItem:primaryDragItem] == -1) ||
528 [primaryDragItem isKindOfClass:[AIListContact class]]) {
529 retVal = NSDragOperationCopy;
531 retVal = NSDragOperationMove;
535 //Otherwise, it's either a move into a group or a manual reordering
536 if (!item || [outlineView isExpandable:item]) {
537 //Figure out where we would insert the dragged item if the sort controller manages the location and it's going into an expandable item
538 AISortController *sortController = [[adium contactController] activeSortController];
539 //XXX If we can sort manually but the sort controller also has some control (e.g. status sort with manual ordering), we should get a hint and make use of it.
540 if (![sortController canSortManually]) {
541 //If we're dragging a group, force a drop onto the contact list itself, and determine the destination location accordingly
542 if ([primaryDragItem isKindOfClass:[AIListGroup class]]) item = nil;
544 int indexForInserting = [sortController indexForInserting:[dragItems objectAtIndex:0]
545 intoObjects:(item ? [item containedObjects] : [[[adium contactController] contactList] containedObjects])];
547 For example, to specify a drop on an item I, you specify item as 1 and index as NSOutlineViewDropOnItemIndex.
548 To specify a drop between child 2 and 3 of item I, you specify item as I and index as 3 (children are a zero-based index).
549 To specify a drop on an unexpandable item 1, you specify item as I and index as NSOutlineViewDropOnItemIndex.
551 [outlineView setDropItem:item dropChildIndex:indexForInserting];
556 retVal = NSDragOperationPrivate;
559 } else if ([types containsObject:NSFilenamesPboardType] ||
560 [types containsObject:NSRTFPboardType]) {
561 retVal = ((item && [item isKindOfClass:[AIListContact class]]) ? NSDragOperationLink : NSDragOperationNone);
563 } else if (!allowBetweenContactDrop) {
564 retVal = NSDragOperationNone;
570 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index
573 NSString *availableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObject:@"AIListObject"]];
575 if ([availableType isEqualToString:@"AIListObject"]) {
576 //Kill the selection now, (in a more finder-esque way)
577 [outlineView deselectAll:nil];
579 //The tree root is not associated with our root contact list group, so we need to make that association here
583 //Move the list object to its new location
584 if ([item isKindOfClass:[AIListGroup class]]) {
585 if (item != [[adium contactController] offlineGroup]) {
586 [[adium contactController] moveListObjects:dragItems intoObject:item index:index];
588 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
595 } else if ([item isKindOfClass:[AIListContact class]]) {
596 NSString *promptTitle;
599 if ([dragItems count] == 1) {
600 promptTitle = [NSString stringWithFormat:AILocalizedString(@"Combine %@ and %@?","Title of the prompt when combining two contacts. Each %@ will be filled with a contact name."), [[dragItems objectAtIndex:0] displayName], [item displayName]];
602 promptTitle = [NSString stringWithFormat:AILocalizedString(@"Combine these contacts with %@?","Title of the prompt when combining two or more contacts with another. %@ will be filled with a contact name."),[item displayName]];
605 //Metacontact creation, prompt the user
606 NSDictionary *context = [NSDictionary dictionaryWithObjectsAndKeys:
608 dragItems, @"dragitems", nil];
610 NSBeginInformationalAlertSheet(promptTitle,
611 AILocalizedString(@"Combine","Button title for accepting the action of combining multiple contacts into a metacontact"),
612 AILocalizedString(@"Cancel",nil),
616 @selector(mergeContactSheetDidEnd:returnCode:contextInfo:),
618 [context retain], //we're responsible for retaining the content object
619 AILocalizedString(@"Once combined, Adium will treat these contacts as a single individual both on your contact list and when sending messages.\n\nYou may un-combine these contacts by getting info on the combined contact.","Explanation of metacontact creation"));
621 } else if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
622 //Drag and Drop file transfer for the contact list.
624 NSArray *files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
625 NSEnumerator *enumerator = [files objectEnumerator];
627 while ((file = [enumerator nextObject])) {
628 AIListContact *targetFileTransferContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
629 forListContact:item];
630 [[adium fileTransferController] sendFile:file toListContact:targetFileTransferContact];
633 } else if ([[[info draggingPasteboard] types] containsObject:NSRTFPboardType]) {
634 //Drag and drop text sending via the contact list.
635 if ([item isKindOfClass:[AIListContact class]]) {
636 /* This will send the message. Alternately, we could just insert it into the text view... */
638 AIContentMessage *messageContent;
640 chat = [[adium chatController] openChatWithContact:(AIListContact *)item
641 onPreferredAccount:YES];
642 messageContent = [AIContentMessage messageInChat:chat
643 withSource:[chat account]
644 destination:[chat listObject]
646 message:[NSAttributedString stringWithData:[[info draggingPasteboard] dataForType:NSRTFPboardType]]
649 [[adium contentController] sendContentObject:messageContent];
656 [super outlineView:outlineView acceptDrop:info item:item childIndex:index];
658 //XXX Is this actually needed?
659 [self contactListChanged:nil];
664 - (void)mergeContactSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
666 NSDictionary *context = (NSDictionary *)contextInfo;
668 if (returnCode == 1) {
669 AIListObject *item = [context objectForKey:@"item"];
670 NSArray *draggedItems = [context objectForKey:@"dragitems"];
671 AIMetaContact *metaContact;
673 //Keep track of where it was before
674 AIListObject<AIContainingObject> *oldContainingObject = [[item containingObject] retain];
675 float oldIndex = [item orderIndex];
677 //Group the destination and then the dragged items into a metaContact
678 metaContact = [[adium contactController] groupListContacts:[[NSArray arrayWithObject:item] arrayByAddingObjectsFromArray:draggedItems]];
680 //Position the metaContact in the group & index the drop point was before
681 [[adium contactController] moveListObjects:[NSArray arrayWithObject:metaContact]
682 intoObject:oldContainingObject
685 [oldContainingObject release];
688 [context release]; //We are responsible for retaining & releasing the context dict
693 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
695 if (object == contactListView && [keyPath isEqualToString:@"desiredHeight"]) {
696 if ([[change objectForKey:NSKeyValueChangeNewKey] intValue] != [[change objectForKey:NSKeyValueChangeOldKey] intValue])
697 [self contactListDesiredSizeChanged];
702 #pragma mark Preferences
704 - (AIContactListWindowStyle)windowStyle
706 NSNumber *windowStyleNumber = [[adium preferenceController] preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE
707 group:PREF_GROUP_APPEARANCE];
708 return (windowStyleNumber ? [windowStyleNumber intValue] : AIContactListWindowStyleStandard);