William Bowling is sharing code with you

Bitbucket is a code hosting site. Unlimited public and private repositories. Free for small teams.

Don't show this again

wbowling / adium (fork of adium / adium)

Fork of Adium for patches/improvements

Clone this repository (size: 338.7 MB): HTTPS / SSH
hg clone https://bitbucket.org/wbowling/adium
hg clone ssh://hg@bitbucket.org/wbowling/adium

adium / Source / AIListController.m

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
/* 
 * Adium is the legal property of its developers, whose names are listed in the copyright file included
 * with this source distribution.
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
 * Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if not,
 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

#import "AIListController.h"
#import "AIAnimatingListOutlineView.h"
#import "AIListWindowController.h"
#import "AIMessageViewController.h"
#import <Adium/AIChat.h>
#import <Adium/AIChatControllerProtocol.h>
#import <Adium/AIContactControllerProtocol.h>
#import <Adium/AIContentControllerProtocol.h>
#import <Adium/AIContentMessage.h>
#import <Adium/AIInterfaceControllerProtocol.h>
#import <Adium/AISortController.h>
#import <Adium/ESFileTransfer.h>
#import <Adium/AIListContact.h>
#import <Adium/AIListGroup.h>
#import <Adium/AIListObject.h>
#import <Adium/AIListBookmark.h>
#import <Adium/AIContactList.h>
#import <Adium/AIMetaContact.h>
#import <Adium/AIListOutlineView.h>
#import <Adium/AIProxyListObject.h>
#import <Adium/AITextAttachmentExtension.h>
#import <AIUtilities/AIAttributedStringAdditions.h>
#import <AIUtilities/AIAutoScrollView.h>
#import <AIUtilities/AIPasteboardAdditions.h>
#import <AIUtilities/AIWindowAdditions.h>
#import <AIUtilities/AIOutlineViewAdditions.h>
#import <AIUtilities/AIObjectAdditions.h>
#import <AIUtilities/AIFunctions.h>
#import <AIUtilities/AIEventAdditions.h>
#import <AIUtilities/AIAttributedStringAdditions.h>

#define EDGE_CATCH_X                                            40.0f
#define EDGE_CATCH_Y                                            40.0f

#define MENU_BAR_HEIGHT                         22

#define KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN     [NSString stringWithFormat:@"Contact List Docked To Bottom:%@", [[self contactList] contentsBasedIdentifier]]

#define PREF_GROUP_APPEARANCE           @"Appearance"

@interface AIListController ()
- (void)promptToCombineItems:(NSArray *)items withContact:(AIListContact *)inContact;
@end

@implementation AIListController


- (id)initWithContactList:(AIListObject<AIContainingObject> *)aContactList
                        inOutlineView:(AIListOutlineView *)inContactListView
                         inScrollView:(AIAutoScrollView *)inScrollView_contactList
                                 delegate:(id<AIListControllerDelegate>)inDelegate
{
        NSParameterAssert(aContactList != nil);
        if ((self = [self initWithContactListView:inContactListView inScrollView:inScrollView_contactList delegate:inDelegate])) {
                [contactListView setDrawHighlightOnlyWhenMain:YES];
                
                self.autoResizeVertically = NO;
                self.autoResizeHorizontally = NO;
                maxWindowWidth = 10000;
                forcedWindowWidth = -1;
                
                //Observe contact list content and display changes
                [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadListObject:) 
                                                                                   name:Contact_ListChanged
                                                                                 object:nil];
                [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadListObject:)
                                                                                   name:Contact_OrderChanged 
                                                                                 object:nil];
                
                [contactListView addObserver:self
                                                  forKeyPath:@"desiredHeight" 
                                                         options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) 
                                                         context:NULL];
                
                [self setContactListRoot:aContactList];

                //Recall how the contact list was docked last time Adium was open
                dockToBottomOfScreen = [[adium.preferenceController preferenceForKey:KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN
                                                                                                                                                 group:PREF_GROUP_WINDOW_POSITIONS] integerValue];
                
                //Observe preference changes
                [adium.preferenceController registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST];
        }

        return self;
}

//Setup the window after it has loaded
- (void)configureViewsAndTooltips
{
        [super configureViewsAndTooltips];
        
        //Listen to when the list window moves (so we can remember which edge we're docked to)
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                                                         selector:@selector(windowDidMove:)
                                                                                                 name:NSWindowDidMoveNotification
                                                                                           object:[contactListView window]];
}

- (void)close
{       
        //Stop observing
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        [adium.preferenceController unregisterPreferenceObserver:self];

        [self autorelease];
}

- (void)dealloc
{
        [contactListView removeObserver:self forKeyPath:@"desiredHeight"];
        
        [super dealloc];
}


- (void)preferencesChangedForGroup:(NSString *)group 
                                                           key:(NSString *)key
                                                        object:(AIListObject *)object 
                                        preferenceDict:(NSDictionary *)prefDict 
                                                 firstTime:(BOOL)firstTime
{
        if (!object)
                [(AIAnimatingListOutlineView *)contactListView setEnableAnimation:[[prefDict objectForKey:KEY_CL_ANIMATE_CHANGES] boolValue]];
}

//Resizing And Positioning ---------------------------------------------------------------------------------------------
#pragma mark Resizing And Positioning
//Dynamically resize the contact list
- (void)contactListDesiredSizeChanged
{
        NSWindow        *theWindow;

    if ((self.autoResizeVertically || self.autoResizeHorizontally) &&
                (theWindow = [contactListView window]) &&
                [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AINoEdges) {
                
                NSRect  currentFrame = [theWindow frame];
        NSRect  desiredFrame = [self _desiredWindowFrameUsingDesiredWidth:(self.autoResizeHorizontally || (forcedWindowWidth != -1))
                                                                                                                        desiredHeight:self.autoResizeVertically];

                if (!NSEqualRects(currentFrame, desiredFrame)) {
                        //We must set the min/max first, otherwise our setFrame will be restricted by them and not produce the
                        //expected results
                        CGFloat toolbarHeight = (self.autoResizeVertically ? [theWindow toolbarHeight] : 0);
                        
                        [theWindow setMinSize:NSMakeSize((self.autoResizeHorizontally ? desiredFrame.size.width : minWindowSize.width),
                                                                                         (self.autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : minWindowSize.height))];
                        [theWindow setMaxSize:NSMakeSize((self.autoResizeHorizontally ? desiredFrame.size.width : 10000),
                                                                                         (self.autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : 10000))];

                        [theWindow setFrame:desiredFrame display:YES animate:NO];
                }
    }
}

/*!
 * @brief The window will be sliding on screen momentarily
 *
 * This is sent by the AIListWindowController. We take this opportunity to perform autosizing as appropriate.
 * The window is actually off-screen and should remain as such; we therefore perform sizing but maintain an appropriate origin such that
 * the window won't be seen.
 */
- (void)contactListWillSlideOnScreen
{
        NSWindow        *theWindow;
        
    if ((self.autoResizeVertically || self.autoResizeHorizontally) &&
                (theWindow = [contactListView window])) {
                NSRect currentFrame, savedFrame, desiredFrame;
                
                
                currentFrame = [theWindow frame];
                /* Pretend, for autosizing purposes, we're where we'll be once we're done sliding on screen. This allows sizing relative to screen edges and the dock
                 * to work properly. We'll return to our previous origin after performing size checking.
                 */
                savedFrame = [(AIListWindowController *)[theWindow windowController] savedFrame];
                [theWindow setFrame:savedFrame display:NO animate:NO];
        
                desiredFrame = [self _desiredWindowFrameUsingDesiredWidth:(self.autoResizeHorizontally || (forcedWindowWidth != -1))
                                                                                                        desiredHeight:self.autoResizeVertically];

                if (!NSEqualRects(savedFrame, desiredFrame)) {
                        /* We must set the min/max first, otherwise our setFrame will be restricted by them and not produce the
                         * expected results
                         */
                        CGFloat toolbarHeight = (self.autoResizeVertically ? [theWindow toolbarHeight] : 0);
                        NSRect offscreenFrame = desiredFrame;
                        [theWindow setMinSize:NSMakeSize((self.autoResizeHorizontally ? desiredFrame.size.width : minWindowSize.width),
                                                                                         (self.autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : minWindowSize.height))];
                        [theWindow setMaxSize:NSMakeSize((self.autoResizeHorizontally ? desiredFrame.size.width : 10000),
                                                                                         (self.autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : 10000))];

                        //Adjust the origin to remain offscreen
                        offscreenFrame.origin.x = NSMinX(currentFrame);

                        if ([(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] & AIMinXEdgeMask) {
                                offscreenFrame.origin.x -= NSWidth(desiredFrame) - NSWidth(currentFrame);
                        }

                        [theWindow setFrame:offscreenFrame display:NO animate:NO];

                        //Note the new desired frame so that we'll slide to that position
                        [(AIListWindowController *)[theWindow windowController] setSavedFrame:desiredFrame];

                } else {
                        //Nothing to do. Return to our actual current frame, unchanged.
                        [theWindow setFrame:currentFrame display:NO animate:NO];
                }
    }
}

//Size for window zoom
- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame
{
    return [self _desiredWindowFrameUsingDesiredWidth:YES desiredHeight:YES];
}

//Window moved, remember which side the user has docked it to
- (void)windowDidMove:(NSNotification *)notification
{
        NSWindow        *theWindow = [contactListView window];
        NSRect          windowFrame = [theWindow frame];
        NSScreen        *theWindowScreen = [theWindow screen];

        NSRect          boundingFrame = [theWindowScreen frame];
        NSRect          visibleBoundingFrame = [theWindowScreen visibleFrame];
        
        AIDockToBottomType oldDockToBottom = dockToBottomOfScreen;

        //First, see if they are now within EDGE_CATCH_Y of the total boundingFrame
        if ((windowFrame.origin.y < boundingFrame.origin.y + EDGE_CATCH_Y) &&
           ((windowFrame.origin.y + windowFrame.size.height) < (boundingFrame.origin.y + boundingFrame.size.height - EDGE_CATCH_Y))) {
                dockToBottomOfScreen = AIDockToBottom_TotalFrame;
                        } else {
                //Then, check for the (possibly smaller) visibleBoundingFrame
                if ((windowFrame.origin.y < visibleBoundingFrame.origin.y + EDGE_CATCH_Y) &&
                   ((windowFrame.origin.y + windowFrame.size.height) < (visibleBoundingFrame.origin.y + visibleBoundingFrame.size.height - EDGE_CATCH_Y))) {
                        dockToBottomOfScreen = AIDockToBottom_VisibleFrame;
                } else {
                        dockToBottomOfScreen = AIDockToBottom_No;
                }
        }

        //Remember how the contact list is currently docked for next time
        if (oldDockToBottom != dockToBottomOfScreen) {
                [adium.preferenceController setPreference:[NSNumber numberWithInteger:dockToBottomOfScreen]
                                                                                         forKey:KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN
                                                                                          group:PREF_GROUP_WINDOW_POSITIONS];
        }
}

//Desired frame of our window - if one of the BOOL values is NO, don't modify that value from the current frame
- (NSRect)_desiredWindowFrameUsingDesiredWidth:(BOOL)useDesiredWidth desiredHeight:(BOOL)useDesiredHeight
{
        NSRect      windowFrame, viewFrame, newWindowFrame, screenFrame, visibleScreenFrame, boundingFrame;
        NSWindow        *theWindow = [contactListView window];
        NSScreen        *currentScreen = [theWindow screen];
        NSInteger                       desiredHeight = [contactListView desiredHeight];
        BOOL            anchorToRightEdge = NO;
        
        windowFrame = [theWindow frame];
        newWindowFrame = windowFrame;
        viewFrame = [scrollView_contactList frame];
        
        if (!currentScreen) currentScreen = [(AIListWindowController *)[theWindow windowController] windowLastScreen];
        if (!currentScreen) currentScreen = [NSScreen mainScreen];

        screenFrame = [currentScreen frame]; 
        visibleScreenFrame = [currentScreen visibleFrame];
        
    //Width
        if (useDesiredWidth) {
                if (forcedWindowWidth != -1) {
                        //If auto-sizing is disabled, use the specified width
                        newWindowFrame.size.width = forcedWindowWidth;
                } else {
                        /* Using horizontal auto-sizing, so find and determine our new width
                         *
                         * First, subtract the current size of the view from our frame
                         */
                        newWindowFrame.size.width -= viewFrame.size.width;
                        
                        //Now, figure out how big the view wants to be and add that to our frame
                        newWindowFrame.size.width += [contactListView desiredWidth];
                        
                        //Don't get bigger than our maxWindowWidth
                        if (newWindowFrame.size.width > maxWindowWidth) {
                                newWindowFrame.size.width = maxWindowWidth;
                        } else if (newWindowFrame.size.width < 0) {
                                newWindowFrame.size.width = 0;  
                        }
                }

                //Anchor to the appropriate screen edge
                anchorToRightEdge = ((currentScreen && ((NSMaxX(windowFrame) + EDGE_CATCH_X) >= NSMaxX(visibleScreenFrame))) ||
                                                         [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AIMaxXEdgeMask);
                if (anchorToRightEdge) {
                        newWindowFrame.origin.x = NSMaxX(windowFrame) - NSWidth(newWindowFrame);
                } else {
                        newWindowFrame.origin.x = NSMinX(windowFrame);
                }
        }

        /*
         * Compute boundingFrame for window
         *
         * If the window is against the left or right edges of the screen AND the user did not dock to the visibleFrame last,
         * we use the full screenFrame as our bound.
         * The edge check is used since most users' docks will not extend to the edges of the screen.
         * Alternately, if the user docked to the total frame last, we can safely use the full screen even if we aren't
         * on the edge.
         */
        BOOL windowOnEdge = ((NSMinX(newWindowFrame) < NSMinX(screenFrame) + EDGE_CATCH_X) ||
                                                 (NSMaxX(newWindowFrame) > (NSMaxX(screenFrame) - EDGE_CATCH_X)));

        if ((windowOnEdge && (dockToBottomOfScreen != AIDockToBottom_VisibleFrame)) ||
           (dockToBottomOfScreen == AIDockToBottom_TotalFrame)) {
                NSArray *screens;

                boundingFrame = screenFrame;

                //We still should not violate the menuBar, so account for it here if we are on the menuBar screen.
                if ((screens = [NSScreen screens]) &&
                        ([screens count]) &&
                        (currentScreen == [screens objectAtIndex:0])) {
                        boundingFrame.size.height -= MENU_BAR_HEIGHT;
                }

        } else {
                boundingFrame = visibleScreenFrame;
        }

        //Height
        if (useDesiredHeight) {
                //Subtract the current size of the view from our frame
                newWindowFrame.size.height -= viewFrame.size.height;

                //Now, figure out how big the view wants to be and add that to our frame
                newWindowFrame.size.height += desiredHeight;

                //Vertical positioning and size if we are placed on a screen
                if (NSHeight(newWindowFrame) >= NSHeight(boundingFrame)) {
                        //If the window is bigger than the screen, keep it on the screen
                        newWindowFrame.size.height = NSHeight(boundingFrame);
                        newWindowFrame.origin.y = NSMinY(boundingFrame);
                } else {
                        //A non-full height window is anchored to the appropriate screen edge
                        if (dockToBottomOfScreen == AIDockToBottom_No) {
                                //If the user did not dock to the bottom in any way last, the origin should move up
                                newWindowFrame.origin.y = NSMaxY(windowFrame) - NSHeight(newWindowFrame);
                        } else {
                                //If the user did dock (either to the full screen or the visible screen), the origin should remain in place.
                                newWindowFrame.origin.y = NSMinY(windowFrame);  
                        }
                }

                //We must never request a height of 0 or OS X will completely move us off the screen
                if (newWindowFrame.size.height == 0) newWindowFrame.size.height = 1;

                //Keep the window from hanging off any Y screen edge (This is optional and could be removed if this annoys people)
                if (NSMaxY(newWindowFrame) > NSMaxY(boundingFrame)) newWindowFrame.origin.y = NSMaxY(boundingFrame) - newWindowFrame.size.height;
                if (NSMinY(newWindowFrame) < NSMinY(boundingFrame)) newWindowFrame.origin.y = NSMinY(boundingFrame);            
        }

        if (useDesiredWidth) {
                /* If the desired height plus any toolbar height exceeds the height we determined, we will be showing a scroller; 
                 * expand horizontally to take that into account.
                 */
                if (desiredHeight + (NSHeight(windowFrame) - NSHeight(viewFrame)) > NSHeight(newWindowFrame)) {
                        CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:[[scrollView_contactList verticalScroller] controlSize]];
                        newWindowFrame.size.width += scrollerWidth;
                        
                        if (anchorToRightEdge) {
                                newWindowFrame.origin.x -= scrollerWidth;
                        }
                }
                
                //We must never request a width of 0 or OS X will completely move us off the screen
                if (newWindowFrame.size.width == 0) newWindowFrame.size.width = 1;

                //Keep the window from hanging off any X screen edge (This is optional and could be removed if this annoys people)
                if (NSMaxX(newWindowFrame) > NSMaxX(boundingFrame)) newWindowFrame.origin.x = NSMaxX(boundingFrame) - NSWidth(newWindowFrame);
                if (NSMinX(newWindowFrame) < NSMinX(boundingFrame)) newWindowFrame.origin.x = NSMinX(boundingFrame);
        }
        
        return newWindowFrame;
}

@synthesize autoResizeHorizontally, autoResizeVertically, autoResizeHorizontallyWithIdleTime, minWindowSize, maxWindowWidth, forcedWindowWidth;

//Content Updating -----------------------------------------------------------------------------------------------------
#pragma mark Content Updating

- (AIListObject<AIContainingObject> *)contactList
{
        return (AIListObject<AIContainingObject> *)contactList;
}

- (void)reloadListObject:(NSNotification *)notification
{
        AIListObject *object = notification.object;
        
        //Treat a nil object as equivalent to the whole contact list
        if (!object || (object == contactList)) {
                [contactListView reloadData];
        } else {
                for (AIProxyListObject *proxyObject in object.proxyObjects)
                        [contactListView reloadItem:proxyObject reloadChildren:YES];
        }
}

/*!
 * @brief List object attributes changed
 *
 * Resize horizontally if desired and the display name changed
 */
- (void)listObjectAttributesChanged:(NSNotification *)notification
{       
        [super listObjectAttributesChanged:notification];
        
        if (((AIListObject *)notification.object).isStranger)
                return;
        
        NSSet *keys = [[notification userInfo] objectForKey:@"Keys"];

        //Resize the contact list horizontally
        if (self.autoResizeHorizontally) {
                if ([keys containsObject:@"Display Name"] || [keys containsObject:@"Long Display Name"] ||
                                (self.autoResizeHorizontallyWithIdleTime && [keys containsObject:@"IdleReadable"])) {
                        [self contactListDesiredSizeChanged];
                }
        }
}

/*!
 * @brief The outline view selection changed
 *
 * On the next run loop, post Interface_ContactSelectionChanged.  Why wait for the next run loop?
 * If we post this notification immediately, our outline view may not yet be key, and the contact controller
 * will return nil for 'selectedListObject'.  If we wait, the outline view will be definitely be set as key, and
 * everything will work as expected.
 */
- (void)outlineViewSelectionDidChange:(NSNotification *)notification
{   
        [[NSNotificationCenter defaultCenter] performSelector:@selector(postNotificationName:object:)
                                                                         withObject:Interface_ContactSelectionChanged
                                                                         withObject:nil
                                                                         afterDelay:0];
}

#pragma mark Drag & Drop

/*! 
 * @brief Method to check if operations need to be performed
 */
- (NSDragOperation)outlineView:(NSOutlineView*)outlineView 
                                  validateDrop:(id <NSDraggingInfo>)info
                                  proposedItem:(AIProxyListObject *)item
                        proposedChildIndex:(NSInteger)index
{
    NSArray                     *types = [[info draggingPasteboard] types];
        NSDragOperation retVal = NSDragOperationNone;
        
        //No dropping into contacts
        BOOL allowBetweenContactDrop = (index == NSOutlineViewDropOnItemIndex);
        AIListObject *proposedListObject = item.listObject;

        if ([types containsObject:@"AIListObject"]) {
                
                id                       dragItem;
                BOOL             hasGroup = NO, hasNonGroup = NO;
                for (dragItem in dragItems) {
                        if ([dragItem isKindOfClass:[AIListGroup class]])
                                hasGroup = YES;
                        if (![dragItem isKindOfClass:[AIListGroup class]])
                                hasNonGroup = YES;
                        if (hasGroup && hasNonGroup) break;
                }
                
                //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)
                if (hasGroup && hasNonGroup) return NSDragOperationNone;
                
                id      primaryDragItem = [dragItems objectAtIndex:0];

                /* If this is a reorder within a metacontact, allow it in all cases. */
                if (([primaryDragItem isKindOfClass:[AIListContact class]] && [proposedListObject isKindOfClass:[AIListContact class]]) &&
                        ([(AIListContact *)primaryDragItem parentContact] == [(AIListContact *)proposedListObject parentContact])) {
                        return ((index != NSOutlineViewDropOnItemIndex) ? NSDragOperationMove : NSDragOperationNone);
                }
                
                if ([primaryDragItem isKindOfClass:[AIListGroup class]]) {
                        NSUInteger dropIndex = index;
                        
                        //Disallow dragging groups into or onto other objects
                        if (item != nil) {
                                AIProxyListObject *currentGroupProxy = item;

                                // Iterate until we reach the highest level.
                                while ([outlineView parentForItem:currentGroupProxy] != nil) {
                                        currentGroupProxy = (AIProxyListObject *)[outlineView parentForItem:currentGroupProxy];
                                }

                                dropIndex = [self.contactList visibleIndexOfObject:currentGroupProxy.listObject];
                        }
                        
                        if ([self.contactList containsObject:primaryDragItem]) {
                                NSUInteger visibleIndex = [self.contactList visibleIndexOfObject:primaryDragItem];
                                
                                // If this is a drop on or directly below, we're not moving anywhere.
                                if (visibleIndex == dropIndex || visibleIndex == dropIndex-1) {
                                        return NSDragOperationNone;
                                }
                        }
                        
                        [outlineView setDropItem:nil dropChildIndex:dropIndex];
                                
                        return NSDragOperationPrivate;
                }

                //We have one or more contacts. Don't allow them to drop on the contact list itself
                if (!item && adium.contactController.useContactListGroups) {
                        /* The user is hovering on the contact list itself.
                         * If groups are shown at all, assuming we have any items in the list at all, she is hovering near but not in a group.
                         *   If (index > 0), the drag is below the end of a group. That group is at (index - 1) in the outline view's root.
                         *   If (index == 0), the drag is at the very top of the contact list.
                         * Do this right by shifting the drop to that group. 
                         */
                        AIProxyListObject *itemAboveProposedIndex = (AIProxyListObject *)[[outlineView dataSource] outlineView:outlineView
                                                                                                                                                                                                                         child:((index > 0) ? (index - 1) : 0)
                                                                                                                                                                                                                        ofItem:nil];
                        if (![itemAboveProposedIndex isKindOfClass:[AIListGroup class]]) {
                                itemAboveProposedIndex = [outlineView parentForItem:itemAboveProposedIndex];
                        }

                        index = ((index > 0) ?
                                         [[outlineView dataSource] outlineView:outlineView numberOfChildrenOfItem:itemAboveProposedIndex] :
                                         NSOutlineViewDropOnItemIndex);
                        
                        item = itemAboveProposedIndex;
                        proposedListObject = item.listObject;

                        [outlineView setDropItem:item dropChildIndex:index];
                }

                if ((index == NSOutlineViewDropOnItemIndex) && [proposedListObject isKindOfClass:[AIListContact class]] &&
                        ([info draggingSource] == [self contactListView])) {
                        //Dropping into a contact or attaching groups: "link"
                        if (([contactListView rowForItem:primaryDragItem] == -1) ||
                                [primaryDragItem isKindOfClass:[AIListContact class]]) {
                                retVal = NSDragOperationLink;

                                if ([primaryDragItem isKindOfClass:[AIListContact class]] &&
                                        [proposedListObject isKindOfClass:[AIListContact class]] &&
                                        [[(AIListContact *)proposedListObject parentContact] isKindOfClass:[AIMetaContact class]]) {
                                        /* Dragging a contact into a contact which is already within a metacontact.
                                         * This should retarget to combine the dragged contact with the metacontact.
                                         */
                                        [outlineView setDropItem:[AIProxyListObject proxyListObjectForListObject:[(AIListContact *)item parentContact]
                                                                                                                                                                inListObject:[(AIListContact *)item parentContact].containingObjects.anyObject]
                                                          dropChildIndex:NSOutlineViewDropOnItemIndex];
                                }

                        } else {
                                retVal = NSDragOperationMove;
                        }
                
                } else if (!item || [outlineView isExpandable:item]) {
                        //Figure out where we would insert the dragged item if the sort controller manages the location and it's going into an expandable item
                        
                        //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.
                        
                        AISortController *sortController = [AISortController activeSortController];
                        AIListObject<AIContainingObject> *container = proposedListObject ? proposedListObject : adium.contactController.contactList;

                        if (!sortController.canSortManually && [container containsObject:[dragItems objectAtIndex:0]]) {
                                // We can't sort manually, and the container already has this item. No operation will take place.
                                retVal = NSDragOperationNone;
                        } else if (![sortController canSortManually]) {
                                // We can't sort manually, but this container doesn't already have the item.
                                
                                NSUInteger insertIndex = [sortController indexForInserting:[dragItems objectAtIndex:0]
                                                                                                                           intoObjects:container.visibleContainedObjects
                                                                                                                           inContainer:container];

                                [outlineView setDropItem:item dropChildIndex:insertIndex];
                                
                                retVal = ([NSEvent optionKey] ? NSDragOperationCopy : NSDragOperationPrivate);
                        } else {
                                // We can sort manually.
                                
                                /* A drop just below a metacontact will appear to be in the group (and should be).
                                 * Adjust to fit reality accordingly.
                                 */
                                if (proposedListObject && [proposedListObject isKindOfClass:[AIMetaContact class]]) {
                                        BOOL isExpanded = [outlineView isItemExpanded:item];
                                        if ((isExpanded && (index == [[outlineView dataSource] outlineView:outlineView
                                                                                                                                numberOfChildrenOfItem:item])) ||
                                                (!isExpanded && (index != NSOutlineViewDropOnItemIndex))) {
                                                
                                                AIProxyListObject<AIContainingObject> *parentObject = [outlineView parentForItem:item];

                                                [outlineView setDropItem:parentObject dropChildIndex:[parentObject visibleIndexOfObject:proposedListObject] + 1];
                                        }
                                }
                                
                                retVal = ([NSEvent optionKey] ? NSDragOperationCopy : NSDragOperationPrivate);
                        }
                } else {
                        retVal = NSDragOperationPrivate;
                }

        } else if ([types containsObject:NSFilenamesPboardType] ||
                           [types containsObject:NSRTFPboardType] ||
                           [types containsObject:NSURLPboardType] ||
                           [types containsObject:NSStringPboardType] ||
                           [types containsObject:AIiTunesTrackPboardType]) {
                retVal = ((proposedListObject && [proposedListObject isKindOfClass:[AIListContact class]]) ? NSDragOperationLink : NSDragOperationNone);

        } else if (!allowBetweenContactDrop) {
                retVal = NSDragOperationNone;
        }

        return retVal;
}

- (NSArray *)arrayOfAllContactsFromArray:(NSArray *)inArray
{
        NSMutableArray *realDragItems = [NSMutableArray array];
        AIListObject   *aDragItem;
        for (aDragItem in inArray) {
                if ([aDragItem isKindOfClass:[AIMetaContact class]]) {
                        [realDragItems addObjectsFromArray:[(AIMetaContact *)aDragItem containedObjects]];

                } else if ([aDragItem isKindOfClass:[AIListContact class]]) {
                        //For listContacts, add all contacts with the same service and UID (on all accounts)
                        [realDragItems addObjectsFromArray:[[adium.contactController allContactsWithService:aDragItem.service 
                                                                                                                                                                                        UID:aDragItem.UID] allObjects]];
                }
        }
        
        return realDragItems;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(AIProxyListObject *)item childIndex:(NSInteger)index
{
        BOOL            success = YES;
        NSPasteboard *draggingPasteboard = [info draggingPasteboard];
        NSString        *availableType;

    if ((availableType = [draggingPasteboard availableTypeFromArray:[NSArray arrayWithObject:@"AIListObject"]])) {
                //Kill the selection now, (in a more finder-esque way)
                [outlineView deselectAll:nil];

                //The tree root is not associated with our root contact list group, so we need to make that association here
                //XXX The contactList is actually an ESObjectWithProperties because it could also be an AIChat.  Confusing.
                if (item == nil) 
                        item = [AIProxyListObject proxyListObjectForListObject:(AIListObject *)contactList inListObject:nil];

                //If we don't have drag items, we are dragging from another instance; build our own dragItems array
                //using the supplied internalObjectIDs
                if (!dragItems) {
                        NSArray                 *dragItemsUniqueIDs;
                        NSMutableArray  *arrayOfDragItems;
                        NSString                *uniqueID;
                        
                        dragItemsUniqueIDs = [draggingPasteboard propertyListForType:@"AIListObjectUniqueIDs"];
                        arrayOfDragItems = [NSMutableArray array];
                        
                        /* XXX We need to know which source group these drag items came from such that we
                         * 1) use the right proxy object
                         * 2) can remove from that source group if moving into a new group
                         */
                        for (uniqueID in dragItemsUniqueIDs) {
                                [arrayOfDragItems addObject:[AIProxyListObject proxyListObjectForListObject:[adium.contactController existingListObjectWithUniqueID:uniqueID]
                                                                                                                                                           inListObject:nil]];
                        }
                        
                        //We will release this when the drag is completed
                        dragItems = [arrayOfDragItems retain];
                }               
                
                [[AIContactObserverManager sharedManager] delayListObjectNotifications];

                //Move the list object to its new location
                if ([item.listObject isKindOfClass:[AIListGroup class]]) {
                        /* Can't drop into the offline group */
                        if (item.listObject != adium.contactController.offlineGroup) {
                                AIListGroup *group = (AIListGroup *)(item.listObject);
                                
                                for (AIProxyListObject *proxyObject in dragItems) {
                                        AIListObject *listObject = proxyObject.listObject;
                                        
                                        NSAssert2([group canContainObject:listObject], @"BUG: Attempting to drop %@ into %@", listObject, group);
                                        
                                        // Allow a drag into a group already containing the list object
                                        // if the group isn't containing -this- proxy.
                                        if (!([group containsObject:listObject] && proxyObject.containingObject == group)) {
                                                if([listObject isKindOfClass:[AIListContact class]]) {
                                                        NSSet *sourceGroups = nil;
                                                        
                                                        if ([NSEvent optionKey]) {
                                                                sourceGroups = [NSSet set];
                                                        } else {
                                                                if ([proxyObject.containingObject isKindOfClass:[AIMetaContact class]] ||
                                                                        [proxyObject.containingObject isKindOfClass:[AIChat class]]) {
                                                                        /*  Passing an empty sourceGroups set is equivalent to a simple addition.
                                                                         *
                                                                         * If we're dragging -from- a meta contact, just do an add;
                                                                         * the move performs the removal from the meta.
                                                                         *
                                                                         * If we're dragging from a chat, just do an add; a move is nonsense.
                                                                         */
                                                                        sourceGroups = [NSSet set];
                                                                } else {
                                                                        sourceGroups = [NSSet setWithObject:proxyObject.containingObject];
                                                                }
                                                        }

                                                        // Contact being moved to a new group.
                                                        // Holding option copies into the new group (like in Finder)
                                                        [adium.contactController moveContact:(AIListContact *)listObject
                                                                                                          fromGroups:sourceGroups
                                                                                                          intoGroups:[NSSet setWithObject:group]];

                                                } else if ([listObject isKindOfClass:[AIListGroup class]]) {                                                    
                                                        // Group being moved to a new detached window.
                                                        NSAssert([group isKindOfClass:[AIContactList class]], @"Target group not an AIContactList");

                                                        [adium.contactController moveGroup:(AIListGroup *)listObject
                                                                                           fromContactList:((AIListGroup *)listObject).contactList 
                                                                                                 toContactList:(AIContactList *)group];
                                                }
                                        }
                                        
                                        [group moveContainedObject:listObject toIndex:index];
                                        [adium.contactController sortListObject:listObject];
                                }
                                
                                [[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged
                                                                                                                                        object:(dragItems.count > 1 ? nil : item.listObject)
                                                                                                                                  userInfo:nil];
                        } else {
                                success = NO;
                        }
                        
                } else if ([item isKindOfClass:[AIMetaContact class]]) {
                        if ([[dragItems objectAtIndex:0] isKindOfClass:[AIListContact class]] &&
                                ([(AIListContact *)[dragItems objectAtIndex:0] parentContact] != item.listObject)) {
                                /* We are dragging a contact into a metacontact, and that contact isn't already part
                                 * of that metacontact. This needs confirmation! */
                                [self promptToCombineItems:dragItems withContact:(AIListContact *)(item.listObject)];

                        } else {
                                /* We're moving things around within a metacontact. Only get the contacts which are actually within it. */
                                NSArray *startingArray = [self arrayOfAllContactsFromArray:dragItems];
                                NSMutableSet *set = [NSMutableSet setWithArray:startingArray];
                                [set intersectSet:[NSSet setWithArray:((AIMetaContact *)item).containedObjects]];

                                for (AIListObject *obj in set) {
                                        [(AIMetaContact *)item moveContainedObject:(AIListContact *)obj toIndex:index];
                                }
                        }
                        [outlineView reloadData];

                } else if ([item isKindOfClass:[AIListContact class]]) {
                        [self promptToCombineItems:dragItems withContact:(AIListContact *)(item.listObject)];
                }
                                 
                [[AIContactObserverManager sharedManager] endListObjectNotificationsDelay];

                
        } else if ((availableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:
                                                                                                                                                                   NSFilenamesPboardType, AIiTunesTrackPboardType, nil]])) {
                //Drag and Drop file transfer for the contact list.
                if ([item isKindOfClass:[AIListContact class]]) {
                        NSArray                 *files = nil;
                        NSString                *file;
                        
                        if ([availableType isEqualToString:NSFilenamesPboardType]) {
                                files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
                                
                        } else if ([availableType isEqualToString:AIiTunesTrackPboardType]) {
                                files = [[info draggingPasteboard] filesFromITunesDragPasteboard];
                        }

                        NSMutableAttributedString *mutableString = [[[NSMutableAttributedString alloc] initWithString:@""] autorelease];
                        
                        for (file in files) {
                                AITextAttachmentExtension   *attachment = [[AITextAttachmentExtension alloc] init];
                                [attachment setPath:file];
                                [attachment setString:[file lastPathComponent]];
                                
                                NSTextAttachmentCell            *cell = [[NSTextAttachmentCell alloc] initImageCell:[attachment iconImage]];
                                [attachment setHasAlternate:NO];
                                [attachment setAttachmentCell:cell];
                                [cell release];
                                
                                [mutableString appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
                                [attachment release];
                        }
                
                        AIChat *chat = [adium.chatController openChatWithContact:(AIListContact *)(item.listObject)
                                                                                                  onPreferredAccount:YES];
                        
                        [chat.chatContainer.messageViewController addToTextEntryView:mutableString];
                        
                        [adium.interfaceController setActiveChat:chat];
                        [NSApp activateIgnoringOtherApps:YES];
                        [NSApp arrangeInFront:nil];

                } else {
                        AILogWithSignature(@"No contact available to receive files");
                        NSBeep();
                }

        } else if ((availableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:NSRTFPboardType,
                                                                                                                                                                   NSURLPboardType, NSStringPboardType, nil]])) {
                //Drag and drop text sending via the contact list.
                if ([item isKindOfClass:[AIListContact class]]) {
                        /* This will send the message. Alternately, we could just insert it into the text view... */
                        NSAttributedString                              *messageAttributedString = nil;
                        
                        if ([availableType isEqualToString:NSRTFPboardType]) {
                                //for RTF data, we want to preserve the formatting, so use dataForType:
                                messageAttributedString = [NSAttributedString stringWithData:[[info draggingPasteboard] dataForType:NSRTFPboardType]];
                        }
                        else if ([availableType isEqualToString:NSURLPboardType]) {
                                //NSURLPboardType contains an NSURL object
                                messageAttributedString = [NSAttributedString stringWithString:[[NSURL URLFromPasteboard:[info draggingPasteboard]] absoluteString]];
                        }
                        else if ([availableType isEqualToString:NSStringPboardType]) {
                                //this is just plain text, so stringForType: works fine
                                messageAttributedString = [NSAttributedString stringWithString:[[info draggingPasteboard] stringForType:NSStringPboardType]];
                        }
                        
                        if(messageAttributedString && [messageAttributedString length] !=0) {
                                AIChat *chat = [adium.chatController openChatWithContact:(AIListContact *)(item.listObject)
                                                                                                          onPreferredAccount:YES];
                                
                                [chat.chatContainer.messageViewController addToTextEntryView:messageAttributedString];
                                
                                [adium.interfaceController setActiveChat:chat];
                                [NSApp activateIgnoringOtherApps:YES];
                                [NSApp arrangeInFront:nil];
                        }
                        else {
                                success = NO;
                        }

                } else {
                        success = NO;
                }
        }
        
        [super outlineView:outlineView acceptDrop:info item:item childIndex:index];
        
    return success;
}

- (void)promptToCombineItems:(NSArray *)items withContact:(AIListContact *)inContact
{
        for (AIListContact *listContact in [items arrayByAddingObject:inContact]) {
                // Make sure all of the items can join the contact.
                if (!listContact.canJoinMetaContacts) {
                        NSRunAlertPanel(AILocalizedString(@"Unable to Combine", nil),
                                                        AILocalizedString(@"%@ is not able to be combined into a meta contact.", nil),
                                                        AILocalizedStringFromTable(@"OK", @"Buttons", "Verb 'OK' on a button"),
                                                        nil,
                                                        nil,
                                                        listContact.displayName);
                        return;
                }
        }
        
        NSString        *promptTitle;
        
        //Appropriate prompt
        if ([items count] == 1) {
                promptTitle = [NSString stringWithFormat:AILocalizedString(@"Combine %@ and %@?","Title of the prompt when combining two contacts. Each %@ will be filled with a contact name."),
                                           [[items objectAtIndex:0] displayName], inContact.displayName];
        } else {
                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."),
                                           inContact.displayName];
        }
        
        //Metacontact creation, prompt the user
        NSDictionary    *context = [NSDictionary dictionaryWithObjectsAndKeys:
                                                                inContact, @"destinationListContact",
                                                                items, @"dragitems", nil];
        
        NSBeginInformationalAlertSheet(promptTitle,
                                                                   AILocalizedString(@"Combine","Button title for accepting the action of combining multiple contacts into a metacontact"),
                                                                   AILocalizedString(@"Cancel",nil),
                                                                   nil,
                                                                   nil,
                                                                   self,
                                                                   @selector(mergeContactSheetDidEnd:returnCode:contextInfo:),
                                                                   nil,
                                                                   [context retain], //we're responsible for retaining the content object
                                                                   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"));
}       

- (void)mergeContactSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
        NSDictionary    *context = (NSDictionary *)contextInfo;

        if (returnCode == 1) {
                AIListObject    *destinationListContact = [context objectForKey:@"destinationListContact"];
                NSArray                 *draggedItems = [context objectForKey:@"dragitems"];

                //Group the destination and then the dragged items into a metaContact
                [adium.contactController groupContacts:[[NSArray arrayWithObject:destinationListContact]
                                                                arrayByAddingObjectsFromArray:[self arrayOfAllContactsFromArray:draggedItems]]];

                //XXX multiple containers: we need to make sure that the metacontacts respect manual ordering correctly
                
                [[NSNotificationCenter defaultCenter] postNotificationName:Contact_OrderChanged object:nil];
        }

        [context release]; //We are responsible for retaining & releasing the context dict
}

#pragma mark KVO

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
        if (object == contactListView && [keyPath isEqualToString:@"desiredHeight"]) {
                if ([[change objectForKey:NSKeyValueChangeNewKey] integerValue] != [[change objectForKey:NSKeyValueChangeOldKey] integerValue])
                        [self contactListDesiredSizeChanged];
                
        }
}

#pragma mark Preferences

- (AIContactListWindowStyle)windowStyle
{
        NSNumber        *windowStyleNumber = [adium.preferenceController preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE 
                                                                                                                                                          group:PREF_GROUP_APPEARANCE];
        return (windowStyleNumber ? [windowStyleNumber integerValue] : AIContactListWindowStyleStandard);
}



@end