Bug Summary

File:Source/AIListController.m
Location:line 683, column 10
Description:dead store

Annotated Source Code

1/*
2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
4 *
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
15 */
16
17#import "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/AIMetaContact.h>
33#import <Adium/AIListOutlineView.h>
34#import <AIUtilities/AIAttributedStringAdditions.h>
35#import <AIUtilities/AIAutoScrollView.h>
36#import <AIUtilities/AIPasteboardAdditions.h>
37#import <AIUtilities/AIWindowAdditions.h>
38#import <AIUtilities/AIOutlineViewAdditions.h>
39#import <AIUtilities/AIObjectAdditions.h>
40#import <AIUtilities/AIFunctions.h>
41
42#define EDGE_CATCH_X 40.0f
43#define EDGE_CATCH_Y 40.0f
44
45#define MENU_BAR_HEIGHT 22
46
47#define KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN [NSString stringWithFormat:@"Contact List Docked To Bottom:%@", [[self contactList] contentsBasedIdentifier]]
48
49#define PREF_GROUP_APPEARANCE @"Appearance"
50
51@interface AIListController (PRIVATE)
52- (void)contactListChanged:(NSNotification *)notification;
53- (void)promptToCombineItems:(NSArray *)items withContact:(AIListContact *)inContact;
54@end
55
56@implementation AIListController
57
58
59- (id)initWithContactList:(AIListObject<AIContainingObject> *)aContactList
60 inOutlineView:(AIListOutlineView *)inContactListView
61 inScrollView:(AIAutoScrollView *)inScrollView_contactList
62 delegate:(id<AIListControllerDelegate>)inDelegate
63{
64 if ((self = [self initWithContactListView:inContactListView inScrollView:inScrollView_contactList delegate:inDelegate])) {
65 [contactListView setDrawHighlightOnlyWhenMain:YES( BOOL ) 1];
66
67 autoResizeVertically = NO( BOOL ) 0;
68 autoResizeHorizontally = NO( BOOL ) 0;
69 maxWindowWidth = 10000;
70 forcedWindowWidth = -1;
71
72 //Observe contact list content and display changes
73 [[adium notificationCenter] addObserver:self selector:@selector(contactListChanged:)
74 name:Contact_ListChanged@ "Contact_ListChanged"
75 object:nil0];
76 [[adium notificationCenter] addObserver:self selector:@selector(contactOrderChanged:)
77 name:Contact_OrderChanged@ "Contact_OrderChanged"
78 object:nil0];
79
80 [contactListView addObserver:self
81 forKeyPath:@"desiredHeight"
82 options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew)
83 context:NULL( ( void * ) 0 )];
84
85 [self setContactListRoot:(aContactList ? aContactList : [[adium contactController] contactList])];
86
87 //Recall how the contact list was docked last time Adium was open
88 dockToBottomOfScreen = [[[adium preferenceController] preferenceForKey:KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN[ NSString stringWithFormat : @ "Contact List Docked To Bottom:%@"
, [ [ self contactList ] contentsBasedIdentifier ] ]
89 group:PREF_GROUP_WINDOW_POSITIONS@ "Window Positions"] intValue];
90
91 //Observe preference changes
92 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_CONTACT_LIST@ "Contact List"];
93 }
94
95 return self;
96}
97
98//Setup the window after it has loaded
99- (void)configureViewsAndTooltips
100{
101 [super configureViewsAndTooltips];
102
103 //Listen to when the list window moves (so we can remember which edge we're docked to)
104 [[NSNotificationCenter defaultCenter] addObserver:self
105 selector:@selector(windowDidMove:)
106 name:NSWindowDidMoveNotification
107 object:[contactListView window]];
108}
109
110- (void)close
111{
112 //Stop observing
113 [[adium notificationCenter] removeObserver:self];
114 [[NSNotificationCenter defaultCenter] removeObserver:self];
115 [[adium preferenceController] unregisterPreferenceObserver:self];
116
117 [self autorelease];
118}
119
120- (void)dealloc
121{
122 [contactListView removeObserver:self forKeyPath:@"desiredHeight"];
123
124 [super dealloc];
125}
126
127
128- (void)preferencesChangedForGroup:(NSString *)group
129 key:(NSString *)key
130 object:(AIListObject *)object
131 preferenceDict:(NSDictionary *)prefDict
132 firstTime:(BOOL)firstTime
133{
134 if (!object)
135 [(AIAnimatingListOutlineView *)contactListView setEnableAnimation:[[prefDict objectForKey:KEY_CL_ANIMATE_CHANGES@ "Animate Changes"] boolValue]];
136}
137
138//Resizing And Positioning ---------------------------------------------------------------------------------------------
139#pragma mark Resizing And Positioning
140//Dynamically resize the contact list
141- (void)contactListDesiredSizeChanged
142{
143 NSWindow *theWindow;
144
145 if ((autoResizeVertically || autoResizeHorizontally) &&
146 (theWindow = [contactListView window]) &&
147 [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AINoEdges) {
148
149 NSRect currentFrame = [theWindow frame];
150 NSRect desiredFrame = [self _desiredWindowFrameUsingDesiredWidth:(autoResizeHorizontally || (forcedWindowWidth != -1))
151 desiredHeight:autoResizeVertically];
152
153 if (!NSEqualRects(currentFrame, desiredFrame)) {
154 //We must set the min/max first, otherwise our setFrame will be restricted by them and not produce the
155 //expected results
156 float toolbarHeight = (autoResizeVertically ? [theWindow toolbarHeight] : 0);
157
158 [theWindow setMinSize:NSMakeSize((autoResizeHorizontally ? desiredFrame.size.width : minWindowSize.width),
159 (autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : minWindowSize.height))];
160 [theWindow setMaxSize:NSMakeSize((autoResizeHorizontally ? desiredFrame.size.width : 10000),
161 (autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : 10000))];
162
163 [theWindow setFrame:desiredFrame display:YES( BOOL ) 1 animate:NO( BOOL ) 0];
164 }
165 }
166}
167
168/*!
169 * @brief The window will be sliding on screen momentarily
170 *
171 * This is sent by the AIListWindowController. We take this opportunity to perform autosizing as appropriate.
172 * The window is actually off-screen and should remain as such; we therefore perform sizing but maintain an appropriate origin such that
173 * the window won't be seen.
174 */
175- (void)contactListWillSlideOnScreen
176{
177 NSWindow *theWindow;
178
179 if ((autoResizeVertically || autoResizeHorizontally) &&
180 (theWindow = [contactListView window])) {
181 NSRect currentFrame, savedFrame, desiredFrame;
182
183
184 currentFrame = [theWindow frame];
185 /* 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
186 * to work properly. We'll return to our previous origin after performing size checking.
187 */
188 savedFrame = [(AIListWindowController *)[theWindow windowController] savedFrame];
189 [theWindow setFrame:savedFrame display:NO( BOOL ) 0 animate:NO( BOOL ) 0];
190
191 desiredFrame = [self _desiredWindowFrameUsingDesiredWidth:(autoResizeHorizontally || (forcedWindowWidth != -1))
192 desiredHeight:autoResizeVertically];
193
194 if (!NSEqualRects(savedFrame, desiredFrame)) {
195 /* We must set the min/max first, otherwise our setFrame will be restricted by them and not produce the
196 * expected results
197 */
198 float toolbarHeight = (autoResizeVertically ? [theWindow toolbarHeight] : 0);
199 NSRect offscreenFrame = desiredFrame;
200 [theWindow setMinSize:NSMakeSize((autoResizeHorizontally ? desiredFrame.size.width : minWindowSize.width),
201 (autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : minWindowSize.height))];
202 [theWindow setMaxSize:NSMakeSize((autoResizeHorizontally ? desiredFrame.size.width : 10000),
203 (autoResizeVertically ? (desiredFrame.size.height - toolbarHeight) : 10000))];
204
205 //Adjust the origin to remain offscreen
206 offscreenFrame.origin.x = NSMinX(currentFrame);
207
208 if ([(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] & AIMinXEdgeMask) {
209 offscreenFrame.origin.x -= NSWidth(desiredFrame) - NSWidth(currentFrame);
210 }
211
212 [theWindow setFrame:offscreenFrame display:NO( BOOL ) 0 animate:NO( BOOL ) 0];
213
214 //Note the new desired frame so that we'll slide to that position
215 [(AIListWindowController *)[theWindow windowController] setSavedFrame:desiredFrame];
216
217 } else {
218 //Nothing to do. Return to our actual current frame, unchanged.
219 [theWindow setFrame:currentFrame display:NO( BOOL ) 0 animate:NO( BOOL ) 0];
220 }
221 }
222}
223
224//Size for window zoom
225- (NSRect)windowWillUseStandardFrame:(NSWindow *)sender defaultFrame:(NSRect)defaultFrame
226{
227 return [self _desiredWindowFrameUsingDesiredWidth:YES( BOOL ) 1 desiredHeight:YES( BOOL ) 1];
228}
229
230//Window moved, remember which side the user has docked it to
231- (void)windowDidMove:(NSNotification *)notification
232{
233 NSWindow *theWindow = [contactListView window];
234 NSRect windowFrame = [theWindow frame];
235 NSScreen *theWindowScreen = [theWindow screen];
236
237 NSRect boundingFrame = [theWindowScreen frame];
238 NSRect visibleBoundingFrame = [theWindowScreen visibleFrame];
239
240 AIDockToBottomType oldDockToBottom = dockToBottomOfScreen;
241
242 //First, see if they are now within EDGE_CATCH_Y of the total boundingFrame
243 if ((windowFrame.origin.y < boundingFrame.origin.y + EDGE_CATCH_Y40.0f) &&
244 ((windowFrame.origin.y + windowFrame.size.height) < (boundingFrame.origin.y + boundingFrame.size.height - EDGE_CATCH_Y40.0f))) {
245 dockToBottomOfScreen = AIDockToBottom_TotalFrame;
246 } else {
247 //Then, check for the (possibly smaller) visibleBoundingFrame
248 if ((windowFrame.origin.y < visibleBoundingFrame.origin.y + EDGE_CATCH_Y40.0f) &&
249 ((windowFrame.origin.y + windowFrame.size.height) < (visibleBoundingFrame.origin.y + visibleBoundingFrame.size.height - EDGE_CATCH_Y40.0f))) {
250 dockToBottomOfScreen = AIDockToBottom_VisibleFrame;
251 } else {
252 dockToBottomOfScreen = AIDockToBottom_No;
253 }
254 }
255
256 //Remember how the contact list is currently docked for next time
257 if (oldDockToBottom != dockToBottomOfScreen) {
258 [[adium preferenceController] setPreference:[NSNumber numberWithInt:dockToBottomOfScreen]
259 forKey:KEY_CONTACT_LIST_DOCKED_TO_BOTTOM_OF_SCREEN[ NSString stringWithFormat : @ "Contact List Docked To Bottom:%@"
, [ [ self contactList ] contentsBasedIdentifier ] ]
260 group:PREF_GROUP_WINDOW_POSITIONS@ "Window Positions"];
261 }
262}
263
264//Desired frame of our window - if one of the BOOL values is NO, don't modify that value from the current frame
265- (NSRect)_desiredWindowFrameUsingDesiredWidth:(BOOL)useDesiredWidth desiredHeight:(BOOL)useDesiredHeight
266{
267 NSRect windowFrame, viewFrame, newWindowFrame, screenFrame, visibleScreenFrame, boundingFrame;
268 NSWindow *theWindow = [contactListView window];
269 NSScreen *currentScreen = [theWindow screen];
270 int desiredHeight = [contactListView desiredHeight];
271 BOOL anchorToRightEdge = NO( BOOL ) 0;
272
273 windowFrame = [theWindow frame];
274 newWindowFrame = windowFrame;
275 viewFrame = [scrollView_contactList frame];
276
277 if (!currentScreen) currentScreen = [(AIListWindowController *)[theWindow windowController] windowLastScreen];
278 if (!currentScreen) currentScreen = [NSScreen mainScreen];
279
280 screenFrame = [currentScreen frame];
281 visibleScreenFrame = [currentScreen visibleFrame];
282
283 //Width
284 if (useDesiredWidth) {
285 if (forcedWindowWidth != -1) {
286 //If auto-sizing is disabled, use the specified width
287 newWindowFrame.size.width = forcedWindowWidth;
288 } else {
289 /* Using horizontal auto-sizing, so find and determine our new width
290 *
291 * First, subtract the current size of the view from our frame
292 */
293 newWindowFrame.size.width -= viewFrame.size.width;
294
295 //Now, figure out how big the view wants to be and add that to our frame
296 newWindowFrame.size.width += [contactListView desiredWidth];
297
298 //Don't get bigger than our maxWindowWidth
299 if (newWindowFrame.size.width > maxWindowWidth) {
300 newWindowFrame.size.width = maxWindowWidth;
301 } else if (newWindowFrame.size.width < 0) {
302 newWindowFrame.size.width = 0;
303 }
304 }
305
306 //Anchor to the appropriate screen edge
307 anchorToRightEdge = ((currentScreen && ((NSMaxX(windowFrame) + EDGE_CATCH_X40.0f) >= NSMaxX(visibleScreenFrame))) ||
308 [(AIListWindowController *)[theWindow windowController] windowSlidOffScreenEdgeMask] == AIMaxXEdgeMask);
309 if (anchorToRightEdge) {
310 newWindowFrame.origin.x = NSMaxX(windowFrame) - NSWidth(newWindowFrame);
311 } else {
312 newWindowFrame.origin.x = NSMinX(windowFrame);
313 }
314 }
315
316 /*
317 * Compute boundingFrame for window
318 *
319 * If the window is against the left or right edges of the screen AND the user did not dock to the visibleFrame last,
320 * we use the full screenFrame as our bound.
321 * The edge check is used since most users' docks will not extend to the edges of the screen.
322 * Alternately, if the user docked to the total frame last, we can safely use the full screen even if we aren't
323 * on the edge.
324 */
325 BOOL windowOnEdge = ((NSMinX(newWindowFrame) < NSMinX(screenFrame) + EDGE_CATCH_X40.0f) ||
326 (NSMaxX(newWindowFrame) > (NSMaxX(screenFrame) - EDGE_CATCH_X40.0f)));
327
328 if ((windowOnEdge && (dockToBottomOfScreen != AIDockToBottom_VisibleFrame)) ||
329 (dockToBottomOfScreen == AIDockToBottom_TotalFrame)) {
330 NSArray *screens;
331
332 boundingFrame = screenFrame;
333
334 //We still should not violate the menuBar, so account for it here if we are on the menuBar screen.
335 if ((screens = [NSScreen screens]) &&
336 ([screens count]) &&
337 (currentScreen == [screens objectAtIndex:0])) {
338 boundingFrame.size.height -= MENU_BAR_HEIGHT22;
339 }
340
341 } else {
342 boundingFrame = visibleScreenFrame;
343 }
344
345 //Height
346 if (useDesiredHeight) {
347 //Subtract the current size of the view from our frame
348 newWindowFrame.size.height -= viewFrame.size.height;
349
350 //Now, figure out how big the view wants to be and add that to our frame
351 newWindowFrame.size.height += desiredHeight;
352
353 //Vertical positioning and size if we are placed on a screen
354 if (NSHeight(newWindowFrame) >= NSHeight(boundingFrame)) {
355 //If the window is bigger than the screen, keep it on the screen
356 newWindowFrame.size.height = NSHeight(boundingFrame);
357 newWindowFrame.origin.y = NSMinY(boundingFrame);
358 } else {
359 //A non-full height window is anchored to the appropriate screen edge
360 if (dockToBottomOfScreen == AIDockToBottom_No) {
361 //If the user did not dock to the bottom in any way last, the origin should move up
362 newWindowFrame.origin.y = NSMaxY(windowFrame) - NSHeight(newWindowFrame);
363 } else {
364 //If the user did dock (either to the full screen or the visible screen), the origin should remain in place.
365 newWindowFrame.origin.y = NSMinY(windowFrame);
366 }
367 }
368
369 //We must never request a height of 0 or OS X will completely move us off the screen
370 if (newWindowFrame.size.height == 0) newWindowFrame.size.height = 1;
371
372 //Keep the window from hanging off any Y screen edge (This is optional and could be removed if this annoys people)
373 if (NSMaxY(newWindowFrame) > NSMaxY(boundingFrame)) newWindowFrame.origin.y = NSMaxY(boundingFrame) - newWindowFrame.size.height;
374 if (NSMinY(newWindowFrame) < NSMinY(boundingFrame)) newWindowFrame.origin.y = NSMinY(boundingFrame);
375 }
376
377 if (useDesiredWidth) {
378 /* If the desired height plus any toolbar height exceeds the height we determined, we will be showing a scroller;
379 * expand horizontally to take that into account. The magic number 2 fixes this method for use with our borderless
380 * windows... I'm not sure why it's needed, but it doesn't hurt anything.
381 */
382 if (desiredHeight + (NSHeight(windowFrame) - NSHeight(viewFrame)) > NSHeight(newWindowFrame) + 2) {
383 float scrollerWidth = [NSScroller scrollerWidthForControlSize:[[scrollView_contactList verticalScroller] controlSize]];
384 newWindowFrame.size.width += scrollerWidth;
385
386 if (anchorToRightEdge) {
387 newWindowFrame.origin.x -= scrollerWidth;
388 }
389 }
390
391 //We must never request a width of 0 or OS X will completely move us off the screen
392 if (newWindowFrame.size.width == 0) newWindowFrame.size.width = 1;
393
394 //Keep the window from hanging off any X screen edge (This is optional and could be removed if this annoys people)
395 if (NSMaxX(newWindowFrame) > NSMaxX(boundingFrame)) newWindowFrame.origin.x = NSMaxX(boundingFrame) - NSWidth(newWindowFrame);
396 if (NSMinX(newWindowFrame) < NSMinX(boundingFrame)) newWindowFrame.origin.x = NSMinX(boundingFrame);
397 }
398
399 return newWindowFrame;
400}
401
402- (void)setMinWindowSize:(NSSize)inSize {
403 minWindowSize = inSize;
404}
405- (void)setMaxWindowWidth:(int)inWidth {
406 maxWindowWidth = inWidth;
407}
408- (void)setAutoresizeHorizontally:(BOOL)flag {
409 autoResizeHorizontally = flag;
410}
411- (void)setAutoresizeVertically:(BOOL)flag {
412 autoResizeVertically = flag;
413}
414- (void)setForcedWindowWidth:(int)inWidth {
415 forcedWindowWidth = inWidth;
416}
417- (void)setAutoresizeHorizontallyWithIdleTime:(BOOL)flag {
418 autoresizeHorizontallyWithIdleTime = flag;
419}
420
421//Content Updating -----------------------------------------------------------------------------------------------------
422#pragma mark Content Updating
423/*!
424 * @brief The entire contact list, or an entire group, changed
425 *
426 * This indicates that an entire group changed -- the contact list is just a giant group, so that includes the entire
427 * contact list changing. Reload the appropriate object.
428 */
429- (void)contactListChanged:(NSNotification *)notification
430{
431 id object = [notification object];
432
433 //Redisplay and resize
434 if (!object || object == contactList) {
435 [contactListView reloadData];
436
437 } else {
438 NSDictionary *userInfo = [notification userInfo];
439 AIListGroup *containingGroup = [userInfo objectForKey:@"ContainingGroup"];
440
441 if (!containingGroup || containingGroup == contactList) {
442 //Reload the whole tree if the containing group is our root
443
444 } else {
445 /* We need to reload the contaning group since this notification is posted when adding and removing objects.
446 * Reloading the actual object that changed will produce no results since it may not be on the list.
447 */
448 [contactListView reloadItem:containingGroup reloadChildren:YES( BOOL ) 1];
449 }
450 }
451}
452
453- (AIListObject<AIContainingObject> *)contactList
454{
455 return contactList;
456}
457
458- (AIListOutlineView *)contactListView
459{
460 return contactListView;
461}
462
463/*!
464 * @brief Order of contacts changed
465 *
466 * The notification's object is the contact whose order changed.
467 * We must reload the group containing that contact in order to correctly update the list.
468 */
469- (void)contactOrderChanged:(NSNotification *)notification
470{
471 id object = [[notification object] containingObject];
472
473 //Treat a nil object as equivalent to the whole contact list
474 if (!object || (object == contactList)) {
475 [contactListView reloadData];
476 } else {
477 [contactListView reloadItem:object reloadChildren:YES( BOOL ) 1];
478 }
479}
480
481/*!
482 * @brief List object attributes changed
483 *
484 * Resize horizontally if desired and the display name changed
485 */
486- (void)listObjectAttributesChanged:(NSNotification *)notification
487{
488 NSSet *keys;
489
490 [super listObjectAttributesChanged:notification];
491
492 keys = [[notification userInfo] objectForKey:@"Keys"];
493
494 //Resize the contact list horizontally
495 if (autoResizeHorizontally) {
496 if ([keys containsObject:@"Display Name"] || [keys containsObject:@"Long Display Name"] ||
497 (autoresizeHorizontallyWithIdleTime && [keys containsObject:@"IdleReadable"])) {
498 [self contactListDesiredSizeChanged];
499 }
500 }
501}
502
503/*!
504 * @brief The outline view selection changed
505 *
506 * On the next run loop, post Interface_ContactSelectionChanged. Why wait for the next run loop?
507 * If we post this notification immediately, our outline view may not yet be key, and the contact controller
508 * will return nil for 'selectedListObject'. If we wait, the outline view will be definitely be set as key, and
509 * everything will work as expected.
510 */
511- (void)outlineViewSelectionDidChange:(NSNotification *)notification
512{
513 [[adium notificationCenter] performSelector:@selector(postNotificationName:object:)
514 withObject:Interface_ContactSelectionChanged@ "Interface_ContactSelectionChanged"
515 withObject:nil0
516 afterDelay:0];
517}
518
519#pragma mark Drag & Drop
520
521/*!
522 * @brief Method to check if operations need to be performed
523 */
524- (NSDragOperation)outlineView:(NSOutlineView*)outlineView validateDrop:(id <NSDraggingInfo>)info proposedItem:(id)item proposedChildIndex:(int)index
525{
526 NSArray *types = [[info draggingPasteboard] types];
527 NSDragOperation retVal = NSDragOperationNone;
528
529 //No dropping into contacts
530 BOOL allowBetweenContactDrop = (index == NSOutlineViewDropOnItemIndex);
531
532 if ([types containsObject:@"AIListObject"]) {
533 BOOL canSortManually = [[[adium contactController] activeSortController] canSortManually];
534
535 NSEnumerator *enumerator = [dragItems objectEnumerator];
536 id dragItem;
537 BOOL hasGroup = NO( BOOL ) 0, hasNonGroup = NO( BOOL ) 0;
538 while ((dragItem = [enumerator nextObject])) {
539 if ([dragItem isKindOfClass:[AIListGroup class]])
540 hasGroup = YES( BOOL ) 1;
541 if (![dragItem isKindOfClass:[AIListGroup class]])
542 hasNonGroup = YES( BOOL ) 1;
543 if (hasGroup && hasNonGroup) break;
544 }
545
546 //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)
547 if (hasGroup && hasNonGroup) return NSDragOperationNone;
548
549 id primaryDragItem = [dragItems objectAtIndex:0];
550
551 /* If this is a reorder within a metacontact, allow it in all cases. */
552 if (([primaryDragItem isKindOfClass:[AIListContact class]] && [item isKindOfClass:[AIListContact class]]) &&
553 ([(AIListContact *)primaryDragItem parentContact] == [(AIListContact *)item parentContact])) {
554 return ((index != NSOutlineViewDropOnItemIndex) ? NSDragOperationMove : NSDragOperationNone);
555 }
556
557 if (index != NSOutlineViewDropOnItemIndex && !canSortManually) {
558 /* We're attempting a resorder, and the sort controller says there is no manual sorting allowed. */
559 return NSDragOperationNone;
560 }
561
562 if ([primaryDragItem isKindOfClass:[AIListGroup class]]) {
563 //Disallow dragging groups into or onto other objects
564 if (item != nil0) {
565 if ([item isKindOfClass:[AIListGroup class]]) {
566 // In between objects
567 [outlineView setDropItem:nil0 dropChildIndex:[[item containingObject] indexOfObject:item]];
568 } else {
569 // On top of an object
570 [outlineView setDropItem:nil0 dropChildIndex:[[[item containingObject] containingObject] indexOfObject:[item containingObject]]];
571 }
572 }
573
574 } else {
575 //We have one or more contacts. Don't allow them to drop on the contact list itself
576 if (!item && [[adium contactController] useContactListGroups]) {
577 /* The user is hovering on the contact list itself.
578 * 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.
579 * If (index > 0), the drag is below the end of a group. That group is at (index - 1) in the outline view's root.
580 * If (index == 0), the drag is at the very top of the contact list.
581 * Do this right by shifting the drop to that group.
582 *
583 *
584 */
585 id itemAboveProposedIndex = [[outlineView dataSource] outlineView:outlineView
586 child:((index > 0) ? (index - 1) : 0)
587 ofItem:nil0];
588 if (![itemAboveProposedIndex isKindOfClass:[AIListGroup class]])
589 itemAboveProposedIndex = [itemAboveProposedIndex containingObject];
590
591 index = ((index > 0) ?
592 [[outlineView dataSource] outlineView:outlineView numberOfChildrenOfItem:itemAboveProposedIndex] :
593 NSOutlineViewDropOnItemIndex);
594
595 [outlineView setDropItem:itemAboveProposedIndex
596 dropChildIndex:index];
597 }
598 }
599
600 if ((index == NSOutlineViewDropOnItemIndex) && [item isKindOfClass:[AIListContact class]] && ([info draggingSource] == [self contactListView])) {
601 //Dropping into a contact or attaching groups: Copy
602 if (([contactListView rowForItem:primaryDragItem] == -1) ||
603 [primaryDragItem isKindOfClass:[AIListContact class]]) {
604 retVal = NSDragOperationCopy;
605
606 if ([primaryDragItem isKindOfClass:[AIListContact class]] &&
607 [item isKindOfClass:[AIListContact class]] &&
608 [[(AIListContact *)item parentContact] isKindOfClass:[AIMetaContact class]]) {
609 /* Dragging a contact into a contact which is already within a metacontact.
610 * This should retarget to combine the dragged contact with the metacontact.
611 */
612 [outlineView setDropItem:[(AIListContact *)item parentContact] dropChildIndex:NSOutlineViewDropOnItemIndex];
613 }
614
615 } else {
616 retVal = NSDragOperationMove;
617 }
618
619 } else {
620 //Otherwise, it's either a move into a group or a manual reordering
621 if (!item || [outlineView isExpandable:item]) {
622 //Figure out where we would insert the dragged item if the sort controller manages the location and it's going into an expandable item
623 AISortController *sortController = [[adium contactController] activeSortController];
624 //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.
625 if (![sortController canSortManually]) {
626 //If we're dragging a group, force a drop onto the contact list itself, and determine the destination location accordingly
627 if ([primaryDragItem isKindOfClass:[AIListGroup class]]) item = nil0;
628
629 int indexForInserting = [sortController indexForInserting:[dragItems objectAtIndex:0]
630 intoObjects:(item ? [item containedObjects] : [[[adium contactController] contactList] containedObjects])];
631 /*
632 For example, to specify a drop on an item I, you specify item as 1 and index as NSOutlineViewDropOnItemIndex.
633 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).
634 To specify a drop on an unexpandable item 1, you specify item as I and index as NSOutlineViewDropOnItemIndex.
635 */
636 [outlineView setDropItem:item dropChildIndex:indexForInserting];
637 }
638 }
639
640 retVal = NSDragOperationPrivate;
641 }
642
643 } else if ([types containsObject:NSFilenamesPboardType] ||
644 [types containsObject:NSRTFPboardType] ||
645 [types containsObject:NSURLPboardType] ||
646 [types containsObject:NSStringPboardType] ||
647 [types containsObject:AIiTunesTrackPboardType@ "CorePasteboardFlavorType 0x6974756E"]) {
648 retVal = ((item && [item isKindOfClass:[AIListContact class]]) ? NSDragOperationLink : NSDragOperationNone);
649
650 } else if (!allowBetweenContactDrop) {
651 retVal = NSDragOperationNone;
652 }
653
654 return retVal;
655}
656
657- (NSArray *)arrayOfAllContactsFromArray:(NSArray *)inArray
658{
659 NSEnumerator *enumerator = [inArray objectEnumerator];
660 NSMutableArray *realDragItems = [NSMutableArray array];
661 AIListObject *aDragItem;
662 while ((aDragItem = [enumerator nextObject])) {
663 if ([aDragItem isKindOfClass:[AIMetaContact class]]) {
664 [realDragItems addObjectsFromArray:[(AIMetaContact *)aDragItem containedObjects]];
665
666 } else if ([aDragItem isKindOfClass:[AIListContact class]]) {
667 //For listContacts, add all contacts with the same service and UID (on all accounts)
668 [realDragItems addObjectsFromArray:[[[adium contactController] allContactsWithService:[aDragItem service]
669 UID:[aDragItem UID]
670 existingOnly:YES( BOOL ) 1] allObjects]];
671 }
672 }
673
674 return realDragItems;
675}
676
677- (BOOL)outlineView:(NSOutlineView *)outlineView acceptDrop:(id <NSDraggingInfo>)info item:(id)item childIndex:(int)index
678{
679 BOOL success = YES( BOOL ) 1;
680 NSPasteboard *draggingPasteboard = [info draggingPasteboard];
681 NSString *availableType;
682
Although the value stored to 'availableType' is used in the enclosing expression, the value is never actually read from 'availableType'
683 if ((availableType = [draggingPasteboard availableTypeFromArray:[NSArray arrayWithObject:@"AIListObject"]])) {
684 //Kill the selection now, (in a more finder-esque way)
685 [outlineView deselectAll:nil0];
686
687 //The tree root is not associated with our root contact list group, so we need to make that association here
688 if (item == nil0)
689 item = contactList;
690
691 //If we don't have drag items, we are dragging from another instance; build our own dragItems array
692 //using the supplied internalObjectIDs
693 if (!dragItems) {
694 NSArray *dragItemsUniqueIDs;
695 NSMutableArray *arrayOfDragItems;
696 NSString *uniqueID;
697 NSEnumerator *enumerator;
698
699 dragItemsUniqueIDs = [draggingPasteboard propertyListForType:@"AIListObjectUniqueIDs"];
700 arrayOfDragItems = [NSMutableArray array];
701
702 enumerator = [dragItemsUniqueIDs objectEnumerator];
703 while ((uniqueID = [enumerator nextObject])) {
704 [arrayOfDragItems addObject:[[adium contactController] existingListObjectWithUniqueID:uniqueID]];
705 }
706
707 //We will release this when the drag is completed
708 dragItems = [arrayOfDragItems retain];
709 }
710
711 //Move the list object to its new location
712 if ([item isKindOfClass:[AIListGroup class]]) {
713 if (item != [[adium contactController] offlineGroup]) {
714 [[adium contactController] moveListObjects:dragItems intoObject:item index:index];
715
716 [[adium notificationCenter] postNotificationName:@"Contact_ListChanged"
717 object:item
718 userInfo:nil0];
719 } else {
720 success = NO( BOOL ) 0;
721 }
722
723 } else if ([item isKindOfClass:[AIMetaContact class]]) {
724 if ([[dragItems objectAtIndex:0] isKindOfClass:[AIListContact class]] &&
725 ([(AIListContact *)[dragItems objectAtIndex:0] parentContact] != item)) {
726 /* We are dragging a contact into a metacontact, and that contact isn't already part
727 * of that metacontact. This needs confirmation! */
728 [self promptToCombineItems:dragItems withContact:item];
729
730 } else {
731 /* We're moving things around within a metacontact. Only get the contacts which are actually within it. */
732 NSArray *startingArray = [self arrayOfAllContactsFromArray:dragItems];
733 NSMutableSet *set = [NSMutableSet setWithArray:startingArray];
734 [set intersectSet:[NSSet setWithArray:[(AIMetaContact *)item containedObjects]]];
735
736 [[adium contactController] moveListObjects:[set allObjects]
737 intoObject:item
738 index:index];
739 }
740 [outlineView reloadData];
741
742 } else if ([item isKindOfClass:[AIListContact class]]) {
743 [self promptToCombineItems:dragItems withContact:item];
744 }
745
746
747 } else if ((availableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:
748 NSFilenamesPboardType, AIiTunesTrackPboardType@ "CorePasteboardFlavorType 0x6974756E", nil0]])) {
749 //Drag and Drop file transfer for the contact list.
750 AIListContact *targetFileTransferContact = [[adium contactController] preferredContactForContentType:CONTENT_FILE_TRANSFER_TYPE@ "File Transfer Type"
751 forListContact:item];
752 if (targetFileTransferContact) {
753 NSArray *files = nil0;
754 NSString *file;
755 NSEnumerator *enumerator;
756
757 if ([availableType isEqualToString:NSFilenamesPboardType]) {
758 files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType];
759
760 } else if ([availableType isEqualToString:AIiTunesTrackPboardType@ "CorePasteboardFlavorType 0x6974756E"]) {
761 files = [[info draggingPasteboard] filesFromITunesDragPasteboard];
762 }
763
764 enumerator = [files objectEnumerator];
765 while ((file = [enumerator nextObject])) {
766 [[adium fileTransferController] sendFile:file toListContact:targetFileTransferContact];
767 }
768
769 } else {
770 AILogWithSignatureAILogWithPrefix ( __PRETTY_FUNCTION__ , @ "No contact available to receive files"
) ;
(@"No contact available to receive files");
771 NSBeep();
772 }
773
774 } else if ((availableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObjects:NSRTFPboardType,
775 NSURLPboardType, NSStringPboardType, nil0]])) {
776 //Drag and drop text sending via the contact list.
777 if ([item isKindOfClass:[AIListContact class]]) {
778 /* This will send the message. Alternately, we could just insert it into the text view... */
779 AIChat *chat;
780 AIContentMessage *messageContent;
781 NSAttributedString *messageAttributedString = nil0;
782
783 if ([availableType isEqualToString:NSRTFPboardType]) {
784 //for RTF data, we want to preserve the formatting, so use dataForType:
785 messageAttributedString = [NSAttributedString stringWithData:[[info draggingPasteboard] dataForType:NSRTFPboardType]];
786 }
787 else if ([availableType isEqualToString:NSURLPboardType]) {
788 //NSURLPboardType contains an NSURL object
789 messageAttributedString = [NSAttributedString stringWithString:[[NSURL URLFromPasteboard:[info draggingPasteboard]] absoluteString]];
790 }
791 else if ([availableType isEqualToString:NSStringPboardType]) {
792 //this is just plain text, so stringForType: works fine
793 messageAttributedString = [NSAttributedString stringWithString:[[info draggingPasteboard] stringForType:NSStringPboardType]];
794 }
795
796 if(messageAttributedString && [messageAttributedString length] !=0) {
797 chat = [[adium chatController] openChatWithContact:(AIListContact *)item
798 onPreferredAccount:YES( BOOL ) 1];
799 messageContent = [AIContentMessage messageInChat:chat
800 withSource:[chat account]
801 destination:[chat listObject]
802 date:nil0
803 message:messageAttributedString
804 autoreply:NO( BOOL ) 0];
805
806 [[adium contentController] sendContentObject:messageContent];
807 }
808 else {
809 success = NO( BOOL ) 0;
810 }
811
812 } else {
813 success = NO( BOOL ) 0;
814 }
815 }
816
817 [super outlineView:outlineView acceptDrop:info item:item childIndex:index];
818
819 //XXX Is this actually needed?
820 [self contactListChanged:nil0];
821
822 return success;
823}
824
825- (void)promptToCombineItems:(NSArray *)items withContact:(AIListContact *)inContact
826{
827 NSString *promptTitle;
828
829 //Appropriate prompt
830 if ([items count] == 1) {
831 promptTitle = [NSString stringWithFormat:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey
: ( @ "Combine %@ and %@?" ) value : @ "" table : ( 0 ) ]
(@"Combine %@ and %@?","Title of the prompt when combining two contacts. Each %@ will be filled with a contact name."),
832 [[items objectAtIndex:0] displayName], [inContact displayName]];
833 } else {
834 promptTitle = [NSString stringWithFormat:AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey
: ( @ "Combine these contacts with %@?" ) value : @ "" table
: ( 0 ) ]
(@"Combine these contacts with %@?","Title of the prompt when combining two or more contacts with another. %@ will be filled with a contact name."),
835 [inContact displayName]];
836 }
837
838 //Metacontact creation, prompt the user
839 NSDictionary *context = [NSDictionary dictionaryWithObjectsAndKeys:
840 inContact, @"item",
841 items, @"dragitems", nil0];
842
843 NSBeginInformationalAlertSheet(promptTitle,
844 AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey
: ( @ "Combine" ) value : @ "" table : ( 0 ) ]
(@"Combine","Button title for accepting the action of combining multiple contacts into a metacontact"),
845 AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey
: ( @ "Cancel" ) value : @ "" table : ( 0 ) ]
(@"Cancel",nil),
846 nil0,
847 nil0,
848 self,
849 @selector(mergeContactSheetDidEnd:returnCode:contextInfo:),
850 nil0,
851 [context retain], //we're responsible for retaining the content object
852 AILocalizedString[ [ NSBundle bundleForClass : [ self class ] ] localizedStringForKey
: ( @ "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."
) value : @ "" table : ( 0 ) ]
(@"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"));
853}
854
855- (void)mergeContactSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
856{
857 NSDictionary *context = (NSDictionary *)contextInfo;
858
859 if (returnCode == 1) {
860 AIListObject *item = [context objectForKey:@"item"];
861 NSArray *draggedItems = [context objectForKey:@"dragitems"];
862 AIMetaContact *metaContact;
863
864 //Keep track of where it was before
865 AIListObject<AIContainingObject> *oldContainingObject = [[item containingObject] retain];
866 float oldIndex = [item orderIndex];
867
868 //Group the destination and then the dragged items into a metaContact
869 metaContact = [[adium contactController] groupListContacts:[[NSArray arrayWithObject:item]
870 arrayByAddingObjectsFromArray:[self arrayOfAllContactsFromArray:draggedItems]]];
871
872 //Position the metaContact in the group & index the drop point was before
873 [[adium contactController] moveListObjects:[NSArray arrayWithObject:metaContact]
874 intoObject:oldContainingObject
875 index:oldIndex];
876
877 [oldContainingObject release];
878 }
879
880 [context release]; //We are responsible for retaining & releasing the context dict
881}
882
883#pragma mark KVO
884
885- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
886{
887 if (object == contactListView && [keyPath isEqualToString:@"desiredHeight"]) {
888 if ([[change objectForKey:NSKeyValueChangeNewKey] intValue] != [[change objectForKey:NSKeyValueChangeOldKey] intValue])
889 [self contactListDesiredSizeChanged];
890
891 }
892}
893
894#pragma mark Preferences
895
896- (AIContactListWindowStyle)windowStyle
897{
898 NSNumber *windowStyleNumber = [[adium preferenceController] preferenceForKey:KEY_LIST_LAYOUT_WINDOW_STYLE@ "Window Style"
899 group:PREF_GROUP_APPEARANCE@ "Appearance"];
900 re