Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / AIListController.m
blob0ca9dbc712b80fcc7477264774ded5376f9a8554
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import "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;
51 @end
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];
63                 
64                 autoResizeVertically = NO;
65                 autoResizeHorizontally = NO;
66                 maxWindowWidth = 10000;
67                 forcedWindowWidth = -1;
68                 
69                 //Observe contact list content and display changes
70                 [[adium notificationCenter] addObserver:self selector:@selector(contactListChanged:) 
71                                                                                    name:Contact_ListChanged
72                                                                                  object:nil];
73                 [[adium notificationCenter] addObserver:self selector:@selector(contactOrderChanged:)
74                                                                                    name:Contact_OrderChanged 
75                                                                                  object:nil];
76                 
77                 [contactListView addObserver:self
78                                                   forKeyPath:@"desiredHeight" 
79                                                          options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) 
80                                                          context:NULL];
81                 
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];
87                 
88                 //Observe preference changes
89                 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST];
90         }
92         return self;
95 //Setup the window after it has loaded
96 - (void)configureViewsAndTooltips
98         [super configureViewsAndTooltips];
99         
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]];
107 - (void)close
108 {       
109     //Stop observing
110     [[adium notificationCenter] removeObserver:self];
111     [[NSNotificationCenter defaultCenter] removeObserver:self];
112         [[adium preferenceController] unregisterPreferenceObserver:self];
114         [self autorelease];
117 - (void)dealloc
119         [contactListView removeObserver:self forKeyPath:@"desiredHeight"];
120         
121         [super dealloc];
125 - (void)preferencesChangedForGroup:(NSString *)group 
126                                                            key:(NSString *)key
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
139         NSWindow        *theWindow;
141     if ((autoResizeVertically || autoResizeHorizontally) &&
142                 (theWindow = [contactListView window]) &&
143                 [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AINoEdges) {
144                 
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
151                         //expected results
152                         float toolbarHeight = (autoResizeVertically ? [theWindow toolbarHeight] : 0);
153                         
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];
160                 }
161     }
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];
179         
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;
186                         } else {
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;
191                 } else {
192                         dockToBottomOfScreen = AIDockToBottom_No;
193                 }
194         }
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];
201         }
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;
212         
213         windowFrame = [theWindow frame];
214         newWindowFrame = windowFrame;
215         viewFrame = [scrollView_contactList frame];
216         
217         if (!currentScreen) currentScreen = [NSScreen mainScreen];
218         
219         screenFrame = [currentScreen frame]; 
220         visibleScreenFrame = [currentScreen visibleFrame];
221         
222     //Width
223         if (useDesiredWidth) {
224                 if (forcedWindowWidth != -1) {
225                         //If auto-sizing is disabled, use the specified width
226                         newWindowFrame.size.width = forcedWindowWidth;
227                 } else {
228                         /* Using horizontal auto-sizing, so find and determine our new width
229                          *
230                          * First, subtract the current size of the view from our frame
231                          */
232                         newWindowFrame.size.width -= viewFrame.size.width;
233                         
234                         //Now, figure out how big the view wants to be and add that to our frame
235                         newWindowFrame.size.width += [contactListView desiredWidth];
236                         
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;  
242                         }
243                 }
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);
250                 } else {
251                         newWindowFrame.origin.x = NSMinX(windowFrame);
252                 }
253         }
255         /*
256          * Compute boundingFrame for window
257          *
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
262          * on the edge.
263          */
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)) {
269                 NSArray *screens;
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]) &&
275                         ([screens count]) &&
276                         (currentScreen == [screens objectAtIndex:0])) {
277                         boundingFrame.size.height -= MENU_BAR_HEIGHT;
278                 }
280         } else {
281                 boundingFrame = visibleScreenFrame;
282         }
284         //Height
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);
297                 } else {
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);
302                         } else {
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);  
305                         }
306                 }
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);            
314         }
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.
320                  */
321                 if (desiredHeight + (NSHeight(windowFrame) - NSHeight(viewFrame)) > NSHeight(newWindowFrame) + 2) {
322                         float scrollerWidth = [NSScroller scrollerWidthForControlSize:[[scrollView_contactList verticalScroller] controlSize]];
323                         newWindowFrame.size.width += scrollerWidth;
324                         
325                         if (anchorToRightEdge) {
326                                 newWindowFrame.origin.x -= scrollerWidth;
327                         }
328                 }
329                 
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);
336         }
337         
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.
364  */
365 - (void)contactListChanged:(NSNotification *)notification
367         id              object = [notification object];
369         //Redisplay and resize
370         if (!object || object == contactList) {
371                 [contactListView reloadData];
373         } else {
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
379                         
380                 } else {
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.
383                          */
384                         [contactListView reloadItem:containingGroup reloadChildren:YES];
385                 }
386         }
389 - (AIListObject<AIContainingObject> *)contactList
391         return 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.
404  */
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];
412         } else {
413                 [contactListView reloadItem:object reloadChildren:YES];
414         }
418  * @brief List object attributes changed
420  * Resize horizontally if desired and the display name changed
421  */
422 - (void)listObjectAttributesChanged:(NSNotification *)notification
424         NSSet   *keys;
425         
426         [super listObjectAttributesChanged:notification];
427         
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];
434                 }
435     }
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.
445  */
446 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
447 {   
448         [[adium notificationCenter] performSelector:@selector(postNotificationName:object:)
449                                                                          withObject:Interface_ContactSelectionChanged
450                                                                          withObject:nil
451                                                                          afterDelay:0];
454 #pragma mark Drag & Drop
456 /*! 
457  * @brief Method to check if operations need to be performed
458  */
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;
463         
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;
472                 }
473                 
474                 NSEnumerator *enumerator = [dragItems objectEnumerator];
475                 id                       dragItem;
476                 BOOL             hasGroup = NO, hasNonGroup = NO;
477                 while ((dragItem = [enumerator nextObject])) {
478                         if ([dragItem isKindOfClass:[AIListGroup class]])
479                                 hasGroup = YES;
480                         if (![dragItem isKindOfClass:[AIListGroup class]])
481                                 hasNonGroup = YES;
482                         if (hasGroup && hasNonGroup) break;
483                 }
484                 
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;
487                 
488                 id      primaryDragItem = [dragItems objectAtIndex:0];
489                 
490                 if ([primaryDragItem isKindOfClass:[AIListGroup class]]) {
491                         //Disallow dragging groups into or onto other objects
492                         if (item != nil) {
493                                 if ([item isKindOfClass:[AIListGroup class]]) {
494                                         // In between objects
495                                         [outlineView setDropItem:nil dropChildIndex:[[item containingObject] indexOfObject:item]];
496                                 } else {
497                                         // On top of an object
498                                         [outlineView setDropItem:nil dropChildIndex:[[[item containingObject] containingObject] indexOfObject:[item containingObject]]];
499                                 }
500                         }
501                         
502                 } else {
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.
509                                  */
510                                 id itemAboveProposedIndex = [outlineView itemAtRow:(index - 1)];
511                                 if (!itemAboveProposedIndex) {
512                                         //At the very end, presumably
513                                         itemAboveProposedIndex = [outlineView itemAtRow:([outlineView numberOfRows] - 1)];
514                                 }
515                                 
516                                 if ([itemAboveProposedIndex isKindOfClass:[AIListGroup class]]) {
517                                         [outlineView setDropItem:itemAboveProposedIndex dropChildIndex:NSOutlineViewDropOnItemIndex];
518                                 } else {
519                                         [outlineView setDropItem:[itemAboveProposedIndex containingObject] dropChildIndex:NSOutlineViewDropOnItemIndex];                                        
520                                 }
521                                 
522                         }
523                 }
524                 
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;
530                         } else {
531                                 retVal = NSDragOperationMove;
532                         }
533                 
534                 } else {
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;
543                                         
544                                         int indexForInserting = [sortController indexForInserting:[dragItems objectAtIndex:0]
545                                                                                                                                   intoObjects:(item ? [item containedObjects] : [[[adium contactController] contactList] containedObjects])];
546                                         /*
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.
550                                          */
551                                         [outlineView setDropItem:item dropChildIndex:indexForInserting];
552                                 }
553                         }
554                         
555                         
556                         retVal = NSDragOperationPrivate;
557                 }
559         } else if ([types containsObject:NSFilenamesPboardType] ||
560                            [types containsObject:NSRTFPboardType] ||
561                            [types containsObject:NSURLPboardType] ||
562                            [types containsObject:NSStringPboardType]) {
563                 retVal = ((item && [item isKindOfClass:[AIListContact class]]) ? NSDragOperationLink : NSDragOperationNone);
565         } else if (!allowBetweenContactDrop) {
566                 retVal = NSDragOperationNone;
567         }
569         return retVal;
572 - (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index
574         BOOL            success = YES;
575         NSString        *availableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObject:@"AIListObject"]];
576         
577     if ([availableType isEqualToString:@"AIListObject"]) {
578                 //Kill the selection now, (in a more finder-esque way)
579                 [outlineView deselectAll:nil];
581                 //The tree root is not associated with our root contact list group, so we need to make that association here
582                 if (item == nil) 
583                         item = contactList;
585                 //Move the list object to its new location
586                 if ([item isKindOfClass:[AIListGroup class]]) {
587                         if (item != [[adium contactController] offlineGroup]) {
588                                 [[adium contactController] moveListObjects:dragItems intoObject:item index:index];
589                                 
590                                 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
591                                                                                                                   object:item
592                                                                                                                 userInfo:nil];
593                         } else {
594                                 success = NO;
595                         }
596                         
597                 } else if ([item isKindOfClass:[AIListContact class]]) {
598                         NSString        *promptTitle;
599                         
600                         //Appropriate prompt
601                         if ([dragItems count] == 1) {
602                                 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]];
603                         } else {
604                                 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                         }
606                         
607                         //Metacontact creation, prompt the user
608                         NSDictionary    *context = [NSDictionary dictionaryWithObjectsAndKeys:
609                                 item, @"item",
610                                 dragItems, @"dragitems", nil];
611                         
612                         NSBeginInformationalAlertSheet(promptTitle,
613                                                                                    AILocalizedString(@"Combine","Button title for accepting the action of combining multiple contacts into a metacontact"),
614                                                                                    AILocalizedString(@"Cancel",nil),
615                                                                                    nil,
616                                                                                    nil,
617                                                                                    self,
618                                                                                    @selector(mergeContactSheetDidEnd:returnCode:contextInfo:),
619                                                                                    nil,
620                                                                                    [context retain], //we're responsible for retaining the content object
621                                                                                    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"));
622                 }
623         } else if ([[[info draggingPasteboard] types] containsObject:NSFilenamesPboardType]) {
624                 //Drag and Drop file transfer for the contact list.
625                 NSString                *file;
626                 NSArray                 *files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
627                 NSEnumerator    *enumerator = [files objectEnumerator];
628                 
629                 while ((file = [enumerator nextObject])) {
630                         AIListContact   *targetFileTransferContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE
631                                                                                                                                                                                                         forListContact:item];
632                         [[adium fileTransferController] sendFile:file toListContact:targetFileTransferContact];
633                 }
635         } else if ([[[info draggingPasteboard] types] containsObject:NSRTFPboardType] ||
636                                 [[[info draggingPasteboard] types] containsObject:NSURLPboardType] ||
637                                 [[[info draggingPasteboard] types] containsObject:NSStringPboardType]) {
638                 //Drag and drop text sending via the contact list.
639                 if ([item isKindOfClass:[AIListContact class]]) {
640                         /* This will send the message. Alternately, we could just insert it into the text view... */
641                         AIChat                                                  *chat;
642                         AIContentMessage                                *messageContent;
643                         NSAttributedString                              *messageAttributedString = nil;
644                         
645                         if([[[info draggingPasteboard] types] containsObject:NSRTFPboardType]) {
646                                 //for RTF data, we want to preserve the formatting, so use dataForType:
647                                 messageAttributedString = [NSAttributedString stringWithData:[[info draggingPasteboard] dataForType:NSRTFPboardType]];
648                         }
649                         else if([[[info draggingPasteboard] types] containsObject:NSURLPboardType]) {
650                                 //NSURLPboardType contains an NSURL object
651                                 messageAttributedString = [NSAttributedString stringWithString:[[NSURL URLFromPasteboard:[info draggingPasteboard]]absoluteString]];
652                         }
653                         else if([[[info draggingPasteboard] types] containsObject:NSStringPboardType]) {
654                                 //this is just plain text, so stringForType: works fine
655                                 messageAttributedString = [NSAttributedString stringWithString:[[info draggingPasteboard]stringForType:NSStringPboardType]];
656                         }
657                         
658                         if(messageAttributedString && [messageAttributedString length] !=0) {
659                                 chat = [[adium chatController] openChatWithContact:(AIListContact *)item
660                                                                                                 onPreferredAccount:YES];
661                                 messageContent = [AIContentMessage messageInChat:chat
662                                                                                                           withSource:[chat account]
663                                                                                                          destination:[chat listObject]
664                                                                                                                         date:nil
665                                                                                                                  message:messageAttributedString
666                                                                                                            autoreply:NO];
667                         
668                                 [[adium contentController] sendContentObject:messageContent];
669                         }
670                         else {
671                                 success = NO;
672                         }
674                 } else {
675                         success = NO;
676                 }
677         }
678         
679         [super outlineView:outlineView acceptDrop:info item:item childIndex:index];
681         //XXX Is this actually needed?
682         [self contactListChanged:nil];
683         
684     return success;
687 - (void)mergeContactSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
689         NSDictionary    *context = (NSDictionary *)contextInfo;
691         if (returnCode == 1) {
692                 AIListObject    *item = [context objectForKey:@"item"];
693                 NSArray                 *draggedItems = [context objectForKey:@"dragitems"];
694                 AIMetaContact   *metaContact;
696                 //Keep track of where it was before
697                 AIListObject<AIContainingObject> *oldContainingObject = [[item containingObject] retain];
698                 float oldIndex = [item orderIndex];
699                 
700                 //Group the destination and then the dragged items into a metaContact
701                 metaContact = [[adium contactController] groupListContacts:[[NSArray arrayWithObject:item] arrayByAddingObjectsFromArray:draggedItems]];
703                 //Position the metaContact in the group & index the drop point was before
704                 [[adium contactController] moveListObjects:[NSArray arrayWithObject:metaContact]
705                                                                                 intoObject:oldContainingObject
706                                                                                          index:oldIndex];
707                 
708                 [oldContainingObject release];
709         }
711         [context release]; //We are responsible for retaining & releasing the context dict
714 #pragma mark KVO
716 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
718         if (object == contactListView && [keyPath isEqualToString:@"desiredHeight"]) {
719                 if ([[change objectForKey:NSKeyValueChangeNewKey] intValue] != [[change objectForKey:NSKeyValueChangeOldKey] intValue])
720                         [self contactListDesiredSizeChanged];
721                 
722         }
725 #pragma mark Preferences
727 - (AIContactListWindowStyle)windowStyle
729         NSNumber        *windowStyleNumber = [[adium preferenceController] preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE 
730                                                                                                                                                           group:PREF_GROUP_APPEARANCE];
731         return (windowStyleNumber ? [windowStyleNumber intValue] : AIContactListWindowStyleStandard);
736 @end